﻿<!DOCTYPE html>
<html>
<head> <meta charset="UTF-8">
<script>
    //  这相当于“条件注释”。在某个条件下用 document.write 动态地写一个
    //
    //				< ! - -
    //
    //	来开启注释（注释掉后续的 js 代码）, 然后用一行静态注释文本
    //
    //			< ! - - 这里随便填 - - >
    //
    //	来结束注释（左端多余的开启注释符无关紧要）。

    // 如果本 html 文件不是被 nwjs 或 electron 打开的, 注释掉网页内容
    if (typeof nw !== 'object'
	&& navigator.userAgent.toLowerCase().indexOf(' electron/') <= -1)
    {
	document.write('<\!\-\-'); // 注释掉后续网页内容，直到碰上结束注释符
    }
</script>

<script>
    // 如果是主窗口, 也开启注释。这是因为以下 style 代码段只用于聊天窗。
    if (opener == null || (typeof nw === 'object' && opener == global)) {
	document.write('<\!\-\-'); // 注释掉后续的 js 代码
    }
</script>

  <style>
/*
    html {
      font-family: sans-serif;
    }

    form {
      width: 600px;
      background: #ccc;
      margin: 0 auto;
      padding: 20px;
      border: 1px solid black;
    }

    form ol {
      padding-left: 0;
    }

    form li, div > p {
      background: #eee;
      display: flex;
      justify-content: space-between;
      margin-bottom: 10px;
      list-style-type: none;
      border: 1px solid black;
    }

    form img {
      height: 64px;
      order: 1;
    }

    form p {
      line-height: 32px;
      padding-left: 10px;
    }

    form label, form button {
      background-color: #7F9CCB;
      padding: 5px 10px;
      border-radius: 5px;
      border: 1px ridge black;
      font-size: 0.8rem;
      height: auto;
    }

    form label:hover, form button:hover {
      background-color: #2D5BA3;
      color: white;
    }

    form label:active, form button:active {
      background-color: #0D3F8F;
      color: white;
    }
*/
    label, button {
      background-color: lightgreen /* #7F9CCB */;
      /* color: #3010D0; */
      padding: 8px 16px;
      border-radius: 8px;
      border: 0px ridge white;
      font-size: 1.0rem;
      height: auto;
    }

    label:hover, button:hover {
      background-color: green /* #2D5BA3 */;
      color: white;
    }

    label:active, button:active {
      background-color: darkgreen /* #0D3F8F */;
      color: white;
    }

  </style>

<!-- 这条注释用于终结前面 js 代码开启的注释，不能删除！ -->

</head>
<body>

<script>
    // 如果本 html 文件是被 nwjs 或 electron 打开的, 注释掉网页内容
    if (typeof nw === 'object'
	|| navigator.userAgent.toLowerCase().indexOf(' electron/') > -1)
    {
	document.write('<\!\-\-'); // 注释掉后续网页内容，直到碰上结束注释符
    }
</script>

	<h4 style="color:tomato">
	本文件不是普通的网页，而是 miao 软件的主模块。</h4>
	<h4 style="color:green">
	miao 是一个局域网即时通讯软件，详情见 README.md 文件。</h4>
	<h4 style="color:tomato">
	请运行 run_miao.bat 来启动 miao。</h4>

<!-- 这条注释用于终结前面 js 代码开启的注释，不能删除！ -->

<script>
    if (opener == null || (typeof nw === 'object' && opener == global)) {
	document.write('<\!\-\-'); // 注释掉后续的 js 代码
    }
</script>

<script>

'use strict';

    if (opener == null || (typeof nw === 'object' && opener == global)) {
	alert('正在打开主窗口。看到这条消息，说明此前的 html 注释方法无效。');
    }
    // 此处放置那些属于聊天窗的“全局”变量（形如 chatWin.variable）
    // 变量需用 var 来声明。let 声明变量, 作用域是块级，在后续代码中无效。
    // 主窗口中不存在这些变量

    //var chat_var_test = 'chat_var_test OK!'; // 测试成功
    //var sendFile = {};
    //var recvFile = {};
    var prevent_close = 1;
    var cmd_mode;
    var cmd_pass = '';
    var tmp_pass = '';
    var pass_errors = 0;
    var pass_intervalID;
    var cmd_intervalID;
    var sending = 0; //0表示发送完成, 负数表示尚未发送, 正数表示有文件正在发送
    var file_send_SN; //跟踪最后一次发送文件时所用的包序号
    var pal_id;
    var encryptmail;

</script>

<!-- 这条注释用于终结前面 js 代码开启的注释，不能删除！ -->

<script>

    //  由于此处开启了动态 html 注释, 在结束注释之前, 后续不可以再使用其它
    //	静态 html 注释。在文件尾部的 “/body” 标签之前有个 html 注释行,
    //	它就是用来结束此处开启的动态 html 注释的。

    //	特别强调: 以下可以随便使用 js 注释，但不可以使用 html 注释！

    if (opener != null && (typeof nw !== 'object' || opener != global)) {
	document.write('<\!\-\-'); // 注释掉后续的 js 代码
    }

    // 如果本 html 文件不是被 nwjs 或 electron 打开的, 注释掉后续的 js 代码
    if (typeof nw !== 'object'
	&& navigator.userAgent.toLowerCase().indexOf(' electron/') < 0)
    {
	document.write('<\!\-\-'); // 注释掉后续的 js 代码
    }
</script>

<script>
    if (opener != null && (typeof nw !== 'object' || opener != global)) {
	alert('正在打开聊天窗。看到这条消息，说明此前的 html 注释方法无效。');
    }
</script>

<script>

'use strict';

    if (typeof nw === 'object') {
	//let version = process.versions['node-webkit'];
	//let v = version.split('.');
	//if (Number(v[0]) > 0 || Number(v[1]) > 46 ||
	//   (Number(v[1]) == 46 && Number(v[2]) > 2)) {
	//    alert ('警告！此版本( ' + version +
	//	   ' ) 的 nwjs 不支持 miao 的部分功能！\r\n\r\n' +
	//	   '请使用 nwjs 0.46.2 或更早的版本。');
	//}
    } else {	// electron
	//let version = process.versions.electron;
	//if (Number(version.split('.')[0]) > 13) {
	//    alert ('严重错误！此版本( ' + version +
	//	   ' ) 的 electron 删除了内建的 remote 模块, \r\n\r\n' +
	//	   '因此 miao 的代码无法运行！\r\n\r\n' +
	//	   '请使用 electron 13.6.9 或更早的版本。');
	//    //window.electronAPI.quitApp();
	//    require('electron').ipcRenderer.send ('quit-app');
	//}
    }

    var iconv;
    let close_counter = 0;
    //const MailParser = require('./mailparser').MailParser;
    const fs = require('fs');
    const path = require('path');
    const readline = require('readline');

    const inspect = require('util').inspect;

    const Doe = {
	host:		'noHost',
	nick:		'noName',
	id:		'Doe',
	IP:		'0.0.0.0',
	port:		2425,
	charset:	'utf8'
    };

    const all = {
	nick:		'all',
	host:		'any',
	id:		'all',
	IP:		'255.255.255.255',
	port:		2425,
	charset:	''
    };

    const who = {
	nick:		'who',
	host:		'any',
	id:		'who',
	IP:		'255.255.255.255',
	port:		2425,
	charset:	''
    };

    let WGROUP='miao';
    let ADDRs;
    let CIDRs;
    //let MACs;
    let GATEWAYs;
    let GUAs; // IPv6 Global Unicast Addresses

    const os = require('os');

    // 登录名
    const login_name = ((process.env.USERNAME ? process.env.USERNAME
			 : (process.env.USER ? process.env.USER
			 : os.userInfo().username)) || 'noName')
			//.replace(/[^a-zA-Z0-9_.]/g, '-');
			.replace(/[\u0000-,/:-@[-`{|}\u007f]/g, '_');
    let hostname;

    try {
	hostname = os.hostname();
    } catch (e) {
	hostname = (process.env.HOSTNAME || process.env.USERDOMAIN);
	let f = '/proc/sys/kernel/hostname';
	if (! hostname && fs.existsSync(f)) {
	    try {
		hostname = fs.readFileSync(f, 'utf8');
	    } catch (e) {
		alert (' 读文件' + f + '失败！\r\n' + e);
	    }
	}
	f = '/etc/hostname';
	if (! hostname && fs.existsSync(f)) {
	    try {
		hostname = fs.readFileSync(f, 'utf8');
	    } catch (e) {
		alert (' 读文件' + f + '失败！\r\n' + e);
	    }
	}
    }
    const me = {
	host:		(hostname || 'noHost')
			//.replace(/[^a-zA-Z0-9_.]/g, '-'),
			.replace(/[\u0000-,/:-@[-`{|}\u007f]/g, '_'),
	nick:		login_name,
	id:		login_name,
	IP:		'0.0.0.0',
	port:		2425,
	charset:	'utf8'
    };

    const HOME = os.homedir(); 
    let OEMCP;
    let OEM_charset;
    let rexec_cwd = process.cwd();

    // 语言
    const LANG = (process.env.LANG || window.navigator.language || 'en-US');
    let date_locale = (LANG.replace(/[.].*$/, '').replace(/_/,'-'));
    let date_options =	{
				weekday:	'long',
				year:		'numeric',
				month:		'2-digit',
				day:		'2-digit',
				hour:		'2-digit',
				minute:		'2-digit',
				second:		'2-digit',
				hour12:		false
			};

    let effective_mailuser;
    let effective_mailpass;
    let privateKey;
    let publicKey;
    let miaoconfig;
    let miaoconfig_needupdate = false;
    let miaoconfig_timeoutID;
    let refresh_timeoutID;
    let f = path.join('..', 'shared', 'miaoconfig.json');
    if (fs.existsSync(f)) {
        try {
	    miaoconfig = fs.readFileSync(f, 'utf8');
        } catch (e) {
	    alert (' 读文件' + f + '失败！\r\n' + e);
        }
        if (miaoconfig) {
	    miaoconfig = JSON.parse(miaoconfig);
        }
    }
    if (! miaoconfig)
	miaoconfig = {};
    var stored_emailuser = miaoconfig.emailuser;
    var stored_emailpass = miaoconfig.emailpass;
    var stored_mailuser = miaoconfig.mailuser;
    var stored_mailpass = miaoconfig.mailpass;
    var stored_smtppass = miaoconfig.smtppass;
    var stored_smtphost = miaoconfig.smtphost;
    var stored_smtpport = miaoconfig.smtpport;
    var stored_imaphost = miaoconfig.imaphost;
    var stored_imapport = miaoconfig.imapport;
    var stored_rexecStaticPass = miaoconfig.rexecStaticPass;
    var stored_mailStartUID = miaoconfig.mailStartUID;
    var stored_randomID = miaoconfig.randomID;
    var stored_nickname = miaoconfig.nickname;
    var stored_workgroup = miaoconfig.workgroup;
    var stored_friends = miaoconfig.friends || '';
    var stored_autorecv = miaoconfig.autorecv;
//  var stored_enableSyncDir = miaoconfig.enableSyncDir;
    var stored_imapsecure = miaoconfig.imapsecure;
    var stored_imaprejectunauth = miaoconfig.imaprejectunauth;
    var stored_smtpsecure = miaoconfig.smtpsecure;
    var stored_smtprejectunauth = miaoconfig.smtprejectunauth;
    var stored_cc = miaoconfig.cc;
    var stored_tmpSubnets = miaoconfig.tmpSubnets;
    var stored_outer_LAN_IP = miaoconfig.outer_LAN_IP;
    var stored_outer_LAN_mask = miaoconfig.outer_LAN_mask;

    if (stored_mailuser && stored_mailpass) {
	effective_mailuser = stored_mailuser;
	effective_mailpass = stored_mailpass;
    } else if (stored_emailuser && stored_emailpass) {
	effective_mailuser = stored_emailuser;
	effective_mailpass = stored_emailpass;
    } else {
	effective_mailuser = undefined;
	effective_mailpass = undefined;
    }

    let Pal;

    const thiswin = (typeof nw === 'object' ? nw.Window.get() : undefined);

    const shell = (typeof nw === 'object' ?
			nw.Shell :
			require('electron').shell);
    // 下面三个变量需要从聊天窗调用, 不可以用 const 或 let 定义为局部变量
    var openItem = shell.openPath || shell.openItem;
    var showItemInFolder = shell.showItemInFolder;
    var openExternal = function (URL) {
	if (! URL)
	    return;
	if (process.platform === 'win32' && /^smb:\/\//i.test(URL)) {
	    let tmp = URL.slice(4).replace(/\//g, '\\');
	    let host = tmp.replace(/^\\\\([^\\]*)(\\.*){0,1}$/, '$1');
	    if (! host || host == tmp)
		return;
	    if (/^\[[0-9A-Fa-f:]+\]/.test(host)) { // IPv6
		host = host.slice(1).replace(/\].*$/, '');
		host = host.replace(/:/g, '-').replace(/%/, 's') +
			'.ipv6-literal.net';
		tmp = '\\\\' + host +
			tmp.replace(/^\\\\([^\\]*)((\\.*){0,1})$/, '$2');
	    }
	    shell.openExternal(tmp);
	} else
	if (process.platform === 'win32' && /^(web)?davs?:\/\//i.test(URL)) {
	    let tmp = URL.replace(/^\w+:/, '').replace(/\//g, '\\');
	    let ssl = /^(web)?davs:/.test(URL);
	    let host = tmp.replace(/^\\\\([^\\]*)(\\.*){0,1}$/, '$1');
	    let port;
	    if (! host || host == tmp)
		return;
	    if (/^\[[0-9A-Fa-f:]+\]/.test(host)) { // IPv6
		port = host.replace(/^.*\]:/, '');
		if (port == host)
		    port = '';
		host = host.slice(1).replace(/\].*$/, '');
		host = host.replace(/:/g, '-').replace(/%/, 's') +
			'.ipv6-literal.net';
		tmp = '\\\\' + host +
			(ssl ? '@SSL' : '') +
			(port ? ('@' + port) : '') +
			//'\\DavWWWRoot' +
			tmp.replace(/^\\\\([^\\]*)((\\.*){0,1})$/, '$2');
	    } else { // IPv4 or DomainName:Port
		let h_p = host.split(':');
		host = h_p[0];
		port = h_p[1];
		tmp = '\\\\' + host +
			(ssl ? '@SSL' : '') +
			(port ? ('@' + port) : '') +
			//'\\DavWWWRoot' +
			tmp.replace(/^\\\\([^\\]*)((\\.*){0,1})$/, '$2');
	    }
	    shell.openExternal(tmp);
	} else
	    shell.openExternal(URL);
    };
    let recvMail = null;
    let sendMail = null;

    let UPnP_WAN_IP;

    const debug_info = window.document.createElement('div');

    function fail (str, chatwin) {
	if (typeof chatwin !== 'object') {
	    debug_info.insertAdjacentHTML ('beforeEnd',
		(new Date()).toLocaleString(date_locale, date_options)
		+ '&nbsp;' + str + '<br/>');
	    return;
	}
	let div_out = chatwin.document.getElementById('outputbox');
	let p = chatwin.document.createElement('p');
	p.innerHTML = '<span style="color:red">' +
		(new Date()).toLocaleString(date_locale, date_options)
		+ '<br/>' + str + '<br/></span>';
	//p.style.color = 'blue';
	p.style.margin = 0;
	//p.style.padding = 0;
	div_out.appendChild(p);
	p.scrollIntoView(false);
    }

    function save_history (str, id, check) {
	let palDirName = Pal[id].nick +
	    id.replace(/@.*$/,'').replace(/:/g,'_');

	let d = path.resolve('..', 'shared', palDirName);
	let history = path.resolve(d, 'history.html');
	if (check) {
	    if (! fs.existsSync(history) || ! fs.statSync(history).isFile())
		return;
	}
	if (! mkdir_recursive(d)) {
	    fail ('创建文件夹 ' + d + ' 失败! ');
	    return;
	}
	if (fs.existsSync(history) && ! fs.statSync(history).isFile()) {
	    fail ('文件 ' + history + ' 不是普通文件，拒绝写入! ');
	    return;
	}
	if (! fs.existsSync(history) || fs.statSync(history).size == 0) {
	    str = '<head><meta charset="UTF-8"></head>\r\n' + str;
	}
	str += '<br/>\r\n';
	try {
	    fs.appendFileSync(history, str);
	} catch (e) {
	    fail ('写文件 ' + history + ' 时失败! ' + e);
	    return;
	}
    }

    function update_miaoconfig (force) {
	if (! force) {
	    if (miaoconfig_timeoutID)
		clearTimeout(miaoconfig_timeoutID);
	    miaoconfig_timeoutID =
		setTimeout(update_miaoconfig, 120000, true);
	    return;
	}
	if (! miaoconfig_needupdate) {
	    fail ('程序有错误: 试图更新无需更新的 miaoconfig 文件。');
	    return;
	}
	miaoconfig_needupdate = false;
	if (miaoconfig_timeoutID) {
	    clearTimeout(miaoconfig_timeoutID);
	    miaoconfig_timeoutID = undefined;
	}
	let f = path.resolve('..', 'shared', 'miaoconfig.json');
	let dir = path.dirname(f);
	if (! mkdir_recursive(dir))
	    fail ('创建文件夹 ' + dir + ' 失败! ');
	try {
	    fs.writeFileSync(f, JSON.stringify(miaoconfig));
	} catch (e) {
	    fail ('写文件' + f + '失败！' + e);
	}
    }

    function bin2text (bin) {
	return btoa (String.fromCharCode(...new Uint8Array(bin)));
    }
    //function bufferToHex(buf) {
    //    var s = '', h = '0123456789ABCDEF';
    //    (new Uint8Array(buf)).forEach((v) => {s += h[v>>4] + h[v&15];});
    //    return s;
    //}

    //fail (JSON.stringify(os.networkInterfaces()));
    // 网络信息有变更时返回 true, 无变更时返回 false
    function updateMyNetInfo () {
	const netinfo = os.networkInterfaces();

	//fail(JSON.stringify(netinfo));
	const old_CIDRs = CIDRs || '';
	const old_GUAs = GUAs || '';
	ADDRs='';
	CIDRs='';
	//MACs='';
	GUAs='';

	for (let i in netinfo) {
	    for (let j in netinfo[i]) {
		const n = netinfo[i][j];
		if (n.internal) continue;
		let cidr = n.cidr;
		if (n.family == 'IPv6') {
		    if (/^[23][0-9A-Fa-f]{3}:/.test(n.address)) {
			if (GUAs) GUAs += ',';
			if (cidr) {
			    GUAs += cidr;
			    continue;
			}
			// 无 cidr, 从 netmask 获得掩码位数
			let mask = n.netmask.toLowerCase();
			if (/::.+$/.test(mask)) {
			    // 严重错误，双冒号右边不该有字符
			    GUAs += n.address + '/128'; // 设定出错兜底
			    continue;
			}
			if (/^[:0]+$/.test(mask)) { // 全是冒号或 0 
			    GUAs += n.address + '/0';
			    continue;
			}
			let a = mask.split(':');
			let x = 0;
			for (let k = 0; k < a.length; k++) {
			    if (a[k] == '')
				break;
			    if (a[k].length != 4) {
				// 严重错误，不该出现少于 4 字节的字段
				x = 128; // 设定出错兜底
				break;
			    }
			    if ((x & 15)) {
				// 严重错误，非 f 的数字出现之后不该有新字段
				x = 128; // 设定出错兜底
				break;
			    }
			    if (a[k] == 'ffff')
				x += 16;
			    else if (a[k] == 'fffe')
				x += 15;
			    else if (a[k] == 'fffc')
				x += 14;
			    else if (a[k] == 'fff8')
				x += 13;
			    else if (a[k] == 'fff0')
				x += 12;
			    else if (a[k] == 'ffe0')
				x += 11;
			    else if (a[k] == 'ffc0')
				x += 10;
			    else if (a[k] == 'ff80')
				x += 9;
			    else if (a[k] == 'ff00')
				x += 8;
			    else if (a[k] == 'fe00')
				x += 7;
			    else if (a[k] == 'fc00')
				x += 6;
			    else if (a[k] == 'f800')
				x += 5;
			    else if (a[k] == 'f000')
				x += 4;
			    else if (a[k] == 'e000')
				x += 3;
			    else if (a[k] == 'c000')
				x += 2;
			    else if (a[k] == '8000')
				x ++;
			    else {
				// 严重错误，不该出现其它字段
				x = 128; // 设定出错兜底
				break;
			    }
			}
			GUAs += n.address + '/' + x;
			continue;
		    }
		    continue;
		}
		if (n.family != 'IPv4') continue;
		if (ADDRs) ADDRs += ',';
		ADDRs += n.address;

		if (! cidr) { // 无 cidr, 从 netmask 获得掩码位数
		    let a = n.netmask.split('.');
		    let x = 0;
		    if (a.length === 4) {
			x += parseInt(a[0]) << 24; 
			x += parseInt(a[1]) << 16; 
			x += parseInt(a[2]) << 8; 
			x += parseInt(a[3]) << 0;
		    } else {
			fail ('严重错误！无效的 IPv4 子网掩码 ' + n.netmask);
			// 出错时由 255.255.255.255 来兜底  
			//x = (255 << 24) | (255 << 16) | (255 << 8) | 255;
			x = 4294967295;
		    }
		    if (typeof Math.clz32 === 'function') {
			x = ~x;
			x = Math.clz32(x);
		    } else if (typeof Math.log2 === 'function') {
			x = -x;
			x >>>= 0;
			if (x > 0)
			    x = 32 - Math.round(Math.log2(x));
		    } else {
			x = -x;
			x >>>= 0;
			if (x > 0)
			    x = 32 - Math.round(Math.log(x) / Math.LN2);
		    }
		    // ip 地址和（用位数表示的）子网掩码拼接起来就是 cidr
		    cidr = n.address + '/' + x;
		}
		if (CIDRs) CIDRs += ',';
		CIDRs += cidr;

		//MACs += ((MACs == '') ? '' : ',') + n.mac.replace(/:/g,'');
	    }
	}

	if (ADDRs && iconv) {
	    UPnP_M_SEARCH ();
	}

	return (old_CIDRs != CIDRs || old_GUAs != GUAs);
    }

    // 定义函数，将好友添加到 Pal 中
    function Pal_add_friends () {
	let friends = stored_friends.split(/[\r\n]+/); // 检查每一行
	for (let i in friends) {
	    if (friends[i].replace(/[\s]+/g, '') == '')
		continue; // 跳过空白行
	    let f = friends[i].split(/\s+/);
	    let id = f[1];
	    if (! miaoconfig[id]) {
		miaoconfig[id] = {};
	    }
	    let found_in_Pal = false;
	    for (let Id in Pal) {
		if (Pal[Id].haskey
		    && Pal[Id].version.split('#')[8] ==
			f[0].replace(/@ethereal\.email$/, '')
		    && Id == id)
		{
		    found_in_Pal = true;
		    break;
		}
	    }
	    if (! found_in_Pal) { // 好友不在 Pal 中
		// 在 Pal 中添加好友的 id
		if (! Pal[id]) {
			Pal[id] = {};
		}
	    }
		if (! Pal[id].nick)
		    Pal[id].nick = f[2];
		if (! Pal[id].wgroup)
		    Pal[id].wgroup = f[3];
		if (! Pal[id].IP)
		    Pal[id].IP = f[4] || '';
		if (! Pal[id].port)
		    Pal[id].port = (Pal[id].IP ? 2425 : '');
		if (! Pal[id].login)
		    Pal[id].login = '';
		if (! Pal[id].host)
		    Pal[id].host = '';
		if (! Pal[id].charset)
		    Pal[id].charset = 'utf8';
		if (! Pal[id].version)
		    Pal[id].version = '1_lbt6_0#131200#' + id +
					'#0#0#0#4001#9#' + f[0]
					  .replace(/@ethereal\.email$/, '');
		if (! Pal[id].chatwin)
		    Pal[id].chatwin = 0;
		if (! Pal[id].myIPs)
		    Pal[id].myIPs = f[5] || '';
		if (! Pal[id].yourIP)
		    Pal[id].yourIP = '';
		if (! Pal[id].yourPort)
		    Pal[id].yourPort = '';
		if (! Pal[id].services)
		    Pal[id].services = '';
		if (! Pal[id].time)
		    Pal[id].time = Date.now();
		if (! Pal[id].mediacy)
		    Pal[id].mediacy = '';
		let keyStr = miaoconfig['publicKey' + id];
		Pal[id].haskey = (keyStr ? 1 : -1);
		if (Pal[id].offline == undefined)
		    Pal[id].offline = (Pal[id].IP ? 0 : 1);
		if (Pal[id].recv == undefined)
		    Pal[id].recv = 0;
		if (Pal[id].send == undefined)
		    Pal[id].send = 0;
	}
    } // Pal_add_friends () 函数定义结束

    let paltable_tbody;

    function paltable_add_row (id) {
	if (typeof id === 'undefined') { // 更新整个表格
	    if (paltable_tbody)
	    	paltable_tbody.innerHTML = '';
	    for (let id in Pal) {
		paltable_add_row (id);
	    }
	    return;
	}
	let tr = window.document.createElement ('tr');
	tr.className = id;
	tr.ondblclick = function (event) { openChat(this.className); };
	tr.onkeyup = function (event) {
		if (event.key == 'Enter')
			openChat(this.className);
		//else
		//	fail (event.key); // 调试输出按键名称
	};
	for (let j in Pal[id]) {
	    let td = window.document.createElement ('td');
	    td.style.background = 'lightgreen';
	    td.style.borderStyle = 'none'; // 也可 td.style.border = 'none';
	    td.innerText = Pal[id][j];
	    tr.appendChild (td);
	}
	tr.title = '双击给 ' + Pal[id].nick + ' 发消息。';
	tr.tabIndex = 0; // 必须用 Tab 获取焦点，否则上下箭头无法获取焦点
	if (! paltable_tbody) {
	    paltable_tbody = window.document.createElement ('tbody');
	    paltable.appendChild (paltable_tbody);
	}
	paltable_tbody.appendChild (tr);
    }

    // 定义函数，更新好友列表
    function updateTable (id) {
	if (typeof id !== 'undefined') {
	    let count = Object.keys(Pal).length + ' 人';
	    if (typeof nw === 'object')	// nwjs 的情况
		thiswin.title = count;
	    else {			// electron 的情况
		require('electron').ipcRenderer.send ('set-title', count);
	    }
	    let tr = window.document.getElementsByClassName(id)[0];
	    if (tr != null) {
		let i = 0;
		for (let j in Pal[id]) {
		    tr.children[i].innerText = Pal[id][j];
		    i++;
		}
		tr.title = '双击给 ' + Pal[id].nick + ' 发消息。';
		return;
	    }
	    paltable_add_row (id);
	    return;
	}

	// 检查是否有聊天窗打开，若无，则清空好友列表
	let chatwin_opened = false;
	if (typeof Pal === 'object') {
	    for (let id in Pal) {
		if (Pal[id].chatwin) {
		    chatwin_opened = true;
		    break;
		}
	    }
	}
	if (! chatwin_opened) { // 不存在活动的聊天窗
	    Pal = {}; // 清空（或初始化）好友列表
	}
	Pal_add_friends();
	paltable_add_row ();		// 更新整个表格

	let count = Object.keys(Pal).length + ' 人';
	if (typeof nw === 'object')	// nwjs 的情况
	    thiswin.title = count;
	else {				// electron 的情况
	    require('electron').ipcRenderer.send ('set-title', count);
	}

	//// 表格排序
	//let intervalID = setInterval ( () => {
	//    if (typeof sorttable === 'object') {
	//	if (! paltable_tbody)
	//	    return;
	//	let c = paltable_tbody.rows.length;
	//	if (c == 0)
	//	    return;
	//	let ipaddr = paltable_tbody.rows[c - 1].cells[2].innerText;
	//	if (! ipaddr)
	//	    return;
	//	if (! /^\d+[.]\d+[.]\d+[.]\d+$/.test(ipaddr))
	//	    return;
	//	clearInterval(intervalID);
	//	sorttable.makeSortable(paltable);
	//    }
	//}, 1000);

    } // updateTable () 函数定义结束


    const dangerous = [
		'',
		'3dsx',
		'8xp',
		'a6p',
		'abs',
		'acc',
		'accde',
		'acx',
		'aex',
		'agt',
		'aif',
		'air',
		'ali',
		'apk',
		'app',
		'application',
		'appref-ms',
		'appx',
		'axf',
		'azw2',
		'bas',
		'bat',
		'bin',
		'bpp',
		'btm',
		'cac',
		'cas',
		'cfxxe',
		'cmd',
		'com',
		'command',
		'cpl',
		'ctl',
		'cxp',
		'dbr',
		'deb',
		'dex',
		'dexe',
		'dld',
		'dll',
		'docm',
		'dol',
		'dot',
		'dotm',
		'dsp',
		'eham',
		'elf',
		'exe',
		'exec',
		'exp',
		'farrun',
		'fmx',
		'fox',
		'fpx',
		'fqy',
		'frm',
		'fxp',
		'g3a',
		'gadget',
		'gambas',
		'gpe',
		'gpu',
		'gpx',
		'gtp',
		'hta',
		'iconfig',
		'ifs',
		'ijc',
		'ime',
		'inf',
		'int',
		'ipod',
		'irx',
		'jar',
		'jax',
		'js',
		'jse',
		'kmd',
		'le',
		'lit',
		'lku',
		'lnk',
		'mex',
		'mexw32',
		'msc',
		'msh',
		'msh1',
		'msh1xml',
		'msh2',
		'msh2xml',
		'mshxml',
		'msi',
		'msp',
		'n',
		'n-gage',
		'ndr',
		'nexe',
		'ns2p',
		'nt',
		'nxe',
		'oat',
		'odex',
		'osx',
		'pdm',
		'pe',
		'pef',
		'pex',
		'pgm',
		'pif',
		'pl',
		'pmb',
		'pot',
		'potm',
		'ppam',
		'ppp9',
		'ppsm',
		'pptm',
		'ppz9',
		'prc',
		'prg',
		'prx',
		'ps1',
		'ps1xml',
		'ps2',
		'ps2xml',
		'psc1',
		'psc2',
		'pva',
		'pwz',
		'pyd',
		'pyz',
		'pyzw',
		'qpkg',
		'r',
		'rbtx',
		'rpm',
		'rtl',
		'run',
		'rxe',
		'ryb',
		's2a',
		'scf',
		'scr',
		'self',
		'shb',
		'shs',
		'sko',
		'sldm',
		'som',
		'sqr',
		'stx',
		'sxx',
		'tcp',
		'tgz',
		'trs',
		'tzx',
		'vb',
		'vbe',
		'vbs',
		'widget',
		'wie',
		'wince',
		'wiz',
		'ws',
		'wsc',
		'wsf',
		'wsh',
		'wwe',
		'x',
		'x86',
		'xap',
		'xip',
		'xlam',
		'xlnk',
		'xlsm',
		'xlt',
		'xltm',
		'zpkg'
	];

    function mkdir_recursive (dpath) {
	let dir = path.dirname(dpath);
	if (dir && dir != '.') {
		if (! fs.existsSync(dir))
		    if (! mkdir_recursive(dir))
			return false;
	}
	if (fs.existsSync(dpath)) {
		if (fs.statSync(dpath).isDirectory())
		    return true;
		// 重命名 dpath, 相当于删除它
		let h;
		try {
		    // 在 dir 下保存文件的旧版本
		    h = fs.mkdtempSync(path.join(dir, path.basename(dpath)+'.'));
		} catch (e) {
		    fail ('异常情况! 创建临时目录 ' + path.basename(dpath)
			+ '.?????? 时失败! 请手动处理! ' + e);
		    return false;
		}
		// Windows 下需要先删除 h 才能重命名为 h
		try {
		    fs.rmdirSync(h);
		} catch (e) {
		    fail ('异常情况! 删除临时目录 ' + h
			+ ' 时失败! 请手动处理! ' + e);
		    return false;
		}
		try {
		    fs.renameSync(dpath, h);
		} catch (e) {
		    fail ('出现意外! 把路径 ' + dpath + ' 重命名为 ' +
			h + ' 时失败! 请手动处理! ' + e);
		    return false;
		}
	}
	try {
		fs.mkdirSync(dpath);
	} catch (e) {
		fail ('出现意外! 创建文件夹 ' + dpath +
			  ' 时失败! 请手动处理! ' + e);
		return false;
	}
	return true;
    }

    const PLEN = 0xF000; // udp 发送文件时, 每个片段(piece)的长度(length)

    // 定义函数, 用 tcp 协议接收文件（当双方都是 miao 用户时, 用 udp 协议）
    function tcpRecvFile (button, id, SN, fno, fname, fsize, time, fattr) {
	// 若对方 miao 发送同步文件, 则 fname 是以 sync 开头的相对路径
	// 将 fname 中的分隔符修改为正确的
	fname = fname.replace(/\/|\\/g, path.sep)
			.replace(/:q/g, '\''); // 恢复单引号
	button.disabled = true; // 禁止再点击此按钮
	let chatwin = button.ownerDocument.defaultView;
	let fail_out = function (str) { fail (str, chatwin); };

	let charset = Pal[id].charset;
	let isMiao = Pal[id].haskey;
	let palDirName = Pal[id].nick +
		id.replace(/@.*$/,'').replace(/:/g,'_');

	// 在 shared 目录下为每个发送者创建专用子目录, 专门接收他发送的文件

	let c = path.join('..', 'shared'); // 设定〖共享〗文件夹在上级目录

	try {
	    fs.mkdirSync(c); // 在上级目录创建〖共享〗文件夹
	} catch (e) {
	    if (e.code != 'EEXIST') { // 失败，而且不是“已存在”
		try {
		    fs.mkdirSync('shared'); // 在当前目录创建〖共享〗文件夹
		} catch (ex) {
		    if (ex.code != 'EEXIST') { // 也失败，而且不是“已存在”
			fail_out
			    ('创建 shared 文件夹失败, 无法接收文件。' + ex);
			button.disabled = false; // 重新启用按钮
			return; // 退出
		    }
		}
		c = 'shared'; // 设定〖共享〗文件夹在当前目录
	    }
	}
	let d = path.join(c, palDirName); // 对方专有文件夹在〖共享〗文件夹下
	try {
	    fs.mkdirSync(d); // 在〖共享〗文件夹下为对方创建专有文件夹
	} catch (e) {
	    if (e.code != 'EEXIST') { // 失败，而且不是“已存在”
		fail_out ('为对方创建专有文件夹失败, 无法接收文件。' + e);
		button.disabled = false; // 重新启用按钮
		return; // 退出
	    }
	}

	function show_buttons_open_file (r) {
		r = path.resolve(r);
		let ext = path.extname(r).slice(1).toLowerCase();

		const button1 = button.nextElementSibling;
		const button2 = button1.nextElementSibling;
		button1.style.display = 'inline'; // 显示<打开>按钮
		button2.style.display = 'inline'; // 显示<打开所在文件夹>按钮
		button1.disabled = true; // 暂时先禁用<打开>按钮, 不允许点击
		button1.onclick = () => {
		    if (typeof nw === 'object') { // nw 的情况
			setTimeout(shell.openItem, 1, r); // 异步执行
		    } else if (shell.openPath) { // electron 9.0+ 的情况
			shell.openPath(r);
		    } else if (shell.openItem) { // electron 8.x- 的情况
			setTimeout(shell.openItem, 1, r); // 异步执行
		    } else {
			// 万一未来 electron 又废除了 openPath, 给出提醒
			fail_out ('严重错误! electron 没有 shell.openItem() '
				+ '和 shell.openPath() 方法!');
		    }
		};
		button2.onclick = () => {
		    if (typeof nw === 'object') { // nw 的情况
			shell.showItemInFolder(r);
		    } else { // electron 某些版本不正常, 它打开的是上层文件夹
			// 解决方法: 把路径解析为 "文件夹" 和 "文件" 两部分,
			// 中间插入 './' (Linux) 或 '.\' (Windows) 就好了。
			if (os.type() == 'Linux')
			    r = path.dirname(r) + path.sep + '.' + path.sep +
				path.basename(r);
			shell.showItemInFolder(r);
		    }
		};
		if (fattr == 2 // 文件夹总是允许打开
		    || ! dangerous.includes(ext)) // 扩展名无潜在危险
			button1.disabled = false; // 启用<打开>按钮, 允许点击
	}

	// 若同名、同长度、同修改时间的普通文件已经存在, 不执行下载
	if (fattr != 2) {
	    let g = path.join(d, fname);
	    if (fs.existsSync(g)) {
		let s = fs.statSync(g);
		if (s.isFile()) {
		    // 若文件长度、修改时间与要下载的文件相同, 则退出
		    let t = Math.floor(s.mtime.getTime()/1000)&(-2);
		    if (fsize == s.size && t == (time & (-2))) {
			button.value =
				'文件已存在, 不再重复接收!';
			//button.disabled = true; // 接收按钮保持禁用
			//相当于文件接收完成, 因此要检查同步文件夹
			//if (isMiao)
			//    dir_sync_start();
			show_buttons_open_file (g);
			return;
		    }
		}
	    }
	}

	
	const f = path.join(d, path.basename(fname) + '.download');
	const log = f.replace(/download$/, 'log');
	let file_exists = false;
	let s_size = 0;
	let fileWriteStream;

	// 用 udp 从 s_size 开始接收文件 (断点续传)
	function udp_get_file () {
	    // 打开文件
	    let fd = null;
	    try {
		fd = fs.openSync(f, s_size ? 'r+' : 'w');
	    } catch (e) {
		fail_out ('打开文件失败, 无法接收文件。' + e);
		button.disabled = false; // 重新启用按钮
		//dir_sync_start(); // 无论接收成功与否, 都检查同步文件夹
		return; // 退出
	    }
	    if (s_size == 0) {
		try {
		    fs.ftruncateSync(fd);
		} catch (e) {
		    fail_out ('截断文件时失败, 无法接收文件。' + e);
		    button.disabled = false; // 重新启用按钮
		    //dir_sync_start(); // 无论接收成功与否, 都检查同步文件夹
		    return; // 退出
		}
	    }
	    if (! Pal[id].recv)
		Pal[id].recv = {};
	    const recvfile = Pal[id].recv[fno - 1] = {};

	    recvfile.fd = fd;
	    recvfile.sz = fsize;
	    recvfile.tm = time; // 精确到秒
	    // udp_socket.on('message',...) 会接收数据、写入文件、关闭文件;
	    // 最后, 再交给 callback 函数做更名等收尾工作。

	    let intervalID;
	    recvfile.cb = function () {
		if (intervalID) {
		    clearInterval(intervalID);
		    intervalID = null;
		}
		//dir_sync_start(); // 无论接收成功与否, 都检查同步文件夹
		if (recvfile.failure != null) {
		    button.value += ' — 接收失败！可重新接收';
		    button.disabled = false; // 重新启用按钮
		    return;
		}
		rename_files();
	    };

	    recvfile.packetID = SN;
	    recvfile.monitor = [];

	    // 通知发送方“我已准备好接收文件”; 发送方收到后会立即发送数据
	    // s_size 应以 PLEN 为边界
	    let old_pieces = -1;
	    let start_p = parseInt(s_size / PLEN);
	    let end_p = Math.ceil(fsize / PLEN);
	    s_size = start_p * PLEN;
	    recvfile.pieces = start_p;
	    recvfile.failure = null;
	    // 连续发送 10 条消息, 间隔 5 秒, 防止 udp 消息被丢弃
	    {
		let tmp_time = Date.now();
		let current_send_SN = send_SN;
		let current_SN = SN;
		let current_fattr = fattr;
		let current_fno = fno;
		let current_s_size = s_size;
		let intervalID = setInterval ( () => {
		    if (Date.now() - tmp_time > 50000)
		    {
			clearInterval(intervalID);
			return;
		    }
		    let tmp_send_SN = send_SN;
		    send_SN = current_SN;
		    sendMsg(me, Pal[id],
			(((current_fattr == 2) ? IPMSG_GETDIRFILES :
				IPMSG_GETFILEDATA)
				| IPMSG_CAPUTF8OPT | IPMSG_UTF8OPT),
			current_SN.toString(16) + ':' +
			current_fno.toString(16) + ':' +
			current_s_size.toString(16) + ':');
		    send_SN = tmp_send_SN;
		}, 5000);
	    }

	    let loops = 0;
	    intervalID = setInterval (function () {
		loops++;
		if (fattr == 2) // 接收文件夹
		    button.value = '已接收 ' + recvfile.pieces + ' 块';
		else
		    button.value = '已接收 ' + (parseInt(
			(recvfile.pieces / end_p * 100000)) / 1000) + '%';
	        if (loops & 0xFF) // (loops % 256)
			return;
		if (old_pieces >= recvfile.pieces) {
		    // 长时间不更新了
		    if (fattr != 2 && recvfile.pieces != end_p) {
			button.value += ' — 接收失败, 可重新接收';
			button.disabled = false; // 重新启用按钮
		    }
		    //dir_sync_start(); // 无论接收成功与否, 都检查同步文件夹
		    if (recvfile.fd != null) {
			try {
			    fs.closeSync(recvfile.fd);
			} catch (e) {
			    fail_out ('接收文件后关闭文件失败。' + e);
			}
		    }
		    recvfile.fd = null;
		    recvfile.cb = null;
		    if (intervalID) {
			clearInterval(intervalID);
			intervalID = null;
		    }
		}
		old_pieces = recvfile.pieces;
	    }, 500);
	} // udp_get_file ()

	function create_dir_files () {
	    // 数据保存在 f 中, 先重命名 f 为 f.dirdata
	    let g = f + '.dirdata';
	    if (fs.existsSync(g)) { // 若存在 g, 则先删除它
		try {
		    fs.unlinkSync(g);
		} catch (e) {
		    fail_out ('删除文件 ' + g + ' 失败! ' + e);
		    button.disabled = false; // 重新启用按钮
		    return; // 退出
		}
	    }
	    try {
		fs.renameSync(f, g);
	    } catch (e) {
		fail_out ('把路径 ' + f + ' 重命名为 ' + g + ' 失败! ' + e);
		button.disabled = false; // 重新启用按钮
		return; // 退出
	    }
	    if (fs.existsSync(f)) {
		fail_out ('把路径 ' + f + ' 重命名为 ' + g + ' 出现意外!');
		button.disabled = false; // 重新启用按钮
		return; // 退出
	    }
	    // 打开 g 读取其中的 dir 数据
	    let dir_data_start = 0;
	    let fd;
	    try {
		fd = fs.openSync(g, 'r');
	    } catch (e) {
		fail_out ('打开文件 ' + g + ' 失败! ' + e);
		button.disabled = false; // 重新启用按钮
		return; // 退出
	    }
	    let stats;
	    try {
		stats = fs.fstatSync(fd);
	    } catch (e) {
		fail_out ('获取文件 ' + g + ' 的 stats 失败! ' + e);
		button.disabled = false; // 重新启用按钮
		fs.close(fd, (err) => {});
		return; // 退出
	    }

	    const len = 0x10000; // 64K
	    let buf = Buffer.alloc(len);
	    try {
		fs.readSync(fd, buf, 0, len, 0);
	    } catch (e) {
		fail_out ('读文件 ' + g + ' 失败! ' + e);
		button.disabled = false; // 重新启用按钮
		fs.close(fd, (err) => {});
		return; // 退出
	    }
	    if (buf[0] == 0x30 && buf[1] == 0x3C) {
		dir_data_start = 0xC04; // 飞秋头部
	    }
	    // 从 dir_data_start 开始处理文件夹信息
	    // 读第一个文件夹项, 它的文件名应该是 fname
	    // 解析文件夹项的长度
	    if (buf[dir_data_start + 4] != 0x3A) { // 冒号
		fail_out ('文件夹数据偏移 04 处不是冒号, 无法处理!');
		button.disabled = false; // 重新启用按钮
		fs.close(fd, (err) => {});
		return; // 退出
	    }
	    let s = buf.toString('utf8', dir_data_start, dir_data_start + 4);
	    let L = parseInt(s, 16);
	    if (isNaN(L) || L < 8 || L > 0xF000) {
		fail_out ('文件夹数据偏移 00 处长度错误, 无法处理!');
		button.disabled = false; // 重新启用按钮
		fs.close(fd, (err) => {});
		return; // 退出
	    }
	    s = buf.slice(dir_data_start + 5, dir_data_start + L);

	    const charset = Pal[id].charset;
	    let entry = (charset == 'utf8' ? s.toString('utf8')
			: iconv.decode (s, charset)).split(':');
	    if (entry[0] != path.basename(fname)) {
		fail_out ('文件夹数据偏移 05 处文件名不匹配, 无法处理!');
		button.disabled = false; // 重新启用按钮
		fs.close(fd, (err) => {});
		return; // 退出
	    }
	    // 联信 feige 在此处使用了非零的值! 可能与其他飞鸽软件不兼容!
	    //if (entry[1] != 0) { // 9 个十六进制字符 '0'
		//button.ownerDocument.defaultView.alert
		//    ('解析文件夹数据时出错, 无法处理! \n');
		//button.disabled = false; // 重新启用按钮
		//fs.close(fd, (err) => {});
		//return; // 退出
	    //}
	    if (entry[2] != 2) { // 1 个十六进制字符 '2'
		fail_out ('文件夹数据的 type 域不是 2, 无法处理!');
		button.disabled = false; // 重新启用按钮
		fs.close(fd, (err) => {});
		return; // 退出
	    }
	    // 创建文件夹 f, 即 fname.download
	    try {
		fs.mkdirSync(f);
	    } catch (e) {
		//if (e.code != 'EEXIST') { // 失败，而且不是“已存在”
		    fail_out ('创建文件夹 ' + f + ' 失败! ' + e);
		    button.disabled = false; // 重新启用按钮
		    fs.close(fd, (err) => {});
		    return; // 退出
		//}
	    }
	    let total_files = 0;
	    let finished_files = 0;
	    let curr_dir = f; // 接下来是在此文件夹下进行操作
	    // 循环处理文件夹内的项目, 起始于 dir_data_start + L
	    for (;;) {
		dir_data_start += L;
		if (dir_data_start > stats.size) {
		    fail_out ('文件项起始位置跨过文件结尾, 无法处理!');
		    button.disabled = false; // 重新启用按钮
		    fs.close(fd, (err) => {});
		    return; // 退出
		}
		let bytesRead;
		try {
		    bytesRead = fs.readSync(fd, buf, 0, len, dir_data_start);
		} catch (e) {
		    fail_out ('读文件 ' + g + ' 失败! ' + e);
		    button.disabled = false; // 重新启用按钮
		    fs.close(fd, (err) => {});
		    return; // 退出
		}
		if (bytesRead < 8 || buf[4] != 0x3A) { // 冒号
		    fail_out ('文件项偏移 04 处不是冒号, 无法处理!');
		    button.disabled = false; // 重新启用按钮
		    fs.close(fd, (err) => {});
		    return; // 退出
		}
		s = buf.toString('utf8', 0, 4);
		L = parseInt(s, 16);
		if (isNaN(L) || L < 8 || L > 0xF000
			|| dir_data_start + L > stats.size) {
		    fail_out ('文件项偏移 00 处长度错误, 无法处理!');
		    button.disabled = false; // 重新启用按钮
		    fs.close(fd, (err) => {});
		    return; // 退出
		}
		s = buf.slice(5, L);
		entry = (charset == 'utf8' ? s.toString('utf8')
			: iconv.decode (s, charset)).split(':');
		// 文件名:文件长度:文件类型:...
		let file_type = parseInt(entry[2], 16);
		// feiq 把系统隐藏文件的类型设定为超过一个字节（0x1001），
		// 导致判断失效。因此，屏蔽掉高位，只保留低字节
		//if (isNaN(file_type) || file_type <= 0 || file_type > 0xFF) {
		//    button.ownerDocument.defaultView.alert
		//	('文件项的 type 不在 0 - 255 的范围, 无法处理! \n');
		//    button.disabled = false; // 重新启用按钮
		//    fs.close(fd, (err) => {});
		//    return; // 退出
		//}
		if (isNaN(file_type)) {
		    fail_out ('错误: type(=' + file_type + ')不是一个数!');
		    button.disabled = false; // 重新启用按钮
		    fs.close(fd, (err) => {});
		    return; // 退出
		}
		file_type &= 0xFF;
		if (file_type == 3) { // 返回父目录
		    if (entry[0] != '.' || entry[1] != 0) {
			fail_out ('返回父目录的项目不是 :.:0:3:, 无法处理!');
			button.disabled = false; // 重新启用按钮
			fs.close(fd, (err) => {});
			return; // 退出
		    }
		    if (curr_dir == f) { // 全部处理完成
			button.value += '完成';
			//fs.closeSync(fd);
			let intervalID = setInterval ( () => {
			    if (finished_files == total_files) {
				clearInterval(intervalID);
				try {
				    fs.closeSync(fd); // 关闭打开的文件 g
				} catch (e) {
				    fail_out ('接收文件夹后关闭文件失败。'
						+ e);
				    return;
				}
				try {
				    fs.unlinkSync(g); // 删除文件 g
				} catch (e) {
				    fail_out ('接收文件夹后删除文件失败。'
						+ e);
				    return;
				}
				rename_files();
			    }
			}, 500);
			return; // 退出
		    }
		    let s = f.length;
		    if (curr_dir.length <= s || curr_dir.slice(0, s) != f) {
			fail_out ('文件项的路径与当前目录名不匹配, 无法处理!');
			button.disabled = false; // 重新启用按钮
			fs.close(fd, (err) => {});
			return; // 退出
		    }
		    curr_dir = path.join (curr_dir, '..'); // 返回父目录
		    continue; // 继续处理下一项
		}
		if (file_type == 2) { // 子目录
		    // 魅族安卓版飞鸽传书使用非零值, 可能与同类软件不兼容
		    //if (entry[1] != 0) {
			//button.ownerDocument.defaultView.alert
			//    ('子目录的长度域不是 0, 无法处理! \n');
			//button.disabled = false; // 重新启用按钮
			//fs.close(fd, (err) => {});
			//return; // 退出
		    //}
		    // 创建子目录 entry[0]
		    let h = path.join(curr_dir, entry[0]);
		    try {
			fs.mkdirSync(h);
		    } catch (e) {
			fail_out ('创建文件夹 ' + h + ' 失败! ' + e);
			button.disabled = false; // 重新启用按钮
			fs.close(fd, (err) => {});
			return; // 退出
		    }
		    curr_dir = h; // 接下来是在此文件夹下进行操作
		    continue; // 继续处理下一项
		}
		if (file_type != 1) { // 其他类型, 例如符号链接等, 忽略掉
		    fail_out ('解析文件夹数据时出现未知文件类型: '
				+ file_type + ', 长度域(hex): '
				+ JSON.stringify(entry[1]));
		    let s = parseInt(entry[1], 16);
		    if (isNaN(s)) {
			fail_out ('未知类型文件的长度域非法, 无法处理!');
			button.disabled = false; // 重新启用按钮
			fs.close(fd, (err) => {});
			return; // 退出
		    }
		    L += s;
		    continue; // 继续处理下一项
		}
		// 普通文件
		s = parseInt(entry[1], 16);
		if (isNaN(s) || s < 0 || dir_data_start + L + s > stats.size) {
		    fail_out ('普通文件的长度域错误, 无法处理!');
		    button.disabled = false; // 重新启用按钮
		    fs.close(fd, (err) => {});
		    return; // 退出
		}
		// 创建文件 entry[0]
		let wf = path.join(curr_dir, entry[0]);
		if (s == 0) { // 长度为 0 的文件
		    let tmp_fd;
		    try {
			tmp_fd = fs.openSync(wf, 'w');
		    } catch (e) {
			fail_out ('创建 0 字节的文件 ' + wf +
				' 失败, 出错信息: ' + e);
			button.disabled = false; // 重新启用按钮
			fs.close(fd, (err) => {});
			return; // 退出
		    }
		    fs.close (tmp_fd, (err) => {});
		    continue; // 继续处理下一项
		}

		// 当 s == 0 时, 下面这个创建读取流的函数崩溃了
		let rs = fs.createReadStream(g, {
				fd: fd,
				autoClose: false,
				start: dir_data_start + L,
				end: dir_data_start + L + s - 1
			});
		rs.on('error', (err) => {
		    fail_out ('读文件 ' + g + ' 出错! ' + err);
		    //ws.end ();
		    //if (! rs.destroyed)
			//rs.destroy();
		});

		rs.on('close', () => {
		    //ws.end ();
		    //if (! rs.destroyed)
			//rs.destroy();
		});

		rs.on('end', () => {
		    //ws.end ();
		    //if (! rs.destroyed)
			//rs.destroy();
		});

		let ws = fs.createWriteStream(wf);

		ws.on('error', (err) => {
		    fail_out ('写文件夹 ' + curr_dir + ' 下的文件 '
				+ entry[0] + ' 失败！' + err);
		    ws.end ();
		    //ws.destroy();
		});
		ws.on('close', () => {
		    finished_files ++;
		    //rs.end ();
		});

		total_files ++;
		rs.pipe (ws);

		L += s;
		continue; // 继续处理下一项
	    }
	    // 上面是无限循环, 不会走到这里
	    button.ownerDocument.defaultView.alert
		('严重错误! 不该执行到此处, 停止处理! \n');
	    button.disabled = false; // 重新启用按钮
	    fs.close(fd, (err) => {});
	    return; // 退出
	} // create_dir_files ()

	function rename_files () {

		if (fsize == 0 && fattr != 2) {
			button.value = '拒绝接收 0 字节的文件';
			return;
		}

		// 当 yourSyncDir 接收完成时, 不更改其 mtime
		let g = path.resolve(d, fname);
		let yourSyncDir = path.resolve('..', 'shared', palDirName,
				palDirName + '.txt');
		if (g != yourSyncDir) {
		    // 改变 f 的 mtime, 也设定 atime
		    try {
			fs.utimesSync(f, (time&(-2)), (time&(-2)));
		    } catch (e) {
			fail_out ('出现意外! 改变 ' + f +
				  ' 的 mtime 时失败! 请手动处理! ' + e);
			//return;
		    }
		}
		// 如果 fname 含有 sync/... 路径, 先创建各级文件夹
		let dir = path.dirname(g);
		if (dir && dir != '.') {
		    if (! mkdir_recursive(dir)) {
			button.value = '创建文件夹失败! 请手动重命名文件';
			return;
		    }
		}
		let r = g;
		label_rename:
		if (path.resolve('..', 'shared', palDirName, 'sync')
			.toLowerCase() == g.toLowerCase()) {
			// g 碰巧就是为 sync 文件夹保留的文件名
			// 此时不可以把 f 更名为 g, 而应该更名为 g.??????
			let h;
			try {
			    h = fs.mkdtempSync(g + '.');
			} catch (e) {
			    fail_out (' 异常情况! 创建临时目录 ' + g
				+ '.?????? 时失败! 请手动处理! ' + e);
			    r = f;
			    break label_rename;
			}
			// Windows 下需要先删除 h 才能重命名为 h
			try {
			    fs.rmdirSync(h);
			} catch (e) {
			    fail_out (' 异常情况! 删除临时目录 ' + h
				+ ' 时失败! 请手动处理! ' + e);
			    r = f;
			    break label_rename;
			}
			try {
			    fs.renameSync(f, h);
			} catch (e) {
			    fail_out ('出现意外! 把路径 ' + f + ' 重命名为 ' +
				h + ' 时失败! 请手动处理! ' + e);
			    r = f;
			    break label_rename;
			}
			// 下载的文件已成功更名为 h
			r = h; // 用户要打开的文件指向更名后的文件
		} else if (! fs.existsSync(g)) {
		    // 不存在 g, 直接把 f 重命名为 g
		    try {
			fs.renameSync(f, g);
		    } catch (e) {
			fail_out ('出现意外! 把路径 ' + f + ' 重命名为 ' + g +
			    ' 时失败! 请手动处理! ' + e);
			r = f;
		    }
		} else {
			// 已存在 g, 先把 g 重命名为 h, 再把 f 重命名为 g
			// 先创建临时目录 h, 再删除它, 从而保证这个名字是
			// 不存在的文件名或目录名
			let h;
			try {
			    //h = fs.mkdtempSync(g + '.');
			    // 在用户专用目录下保存文件的旧版本
			    h = fs.mkdtempSync(f.replace(/download$/, ''));
			} catch (e) {
			    fail_out (' 异常情况! 创建临时目录 ' +
				f.replace(/download$/, '')
				+ '?????? 时失败! 请手动处理! ' + e);
			    r = f;
			    break label_rename;
			}
			// Windows 下需要先删除 h 才能重命名为 h
			try {
			    fs.rmdirSync(h);
			} catch (e) {
			    fail_out (' 异常情况! 删除临时目录 ' + h
				+ ' 时失败! 请手动处理! ' + e);
			    r = f;
			    break label_rename;
			}
			try {
			    fs.renameSync(g, h);
			} catch (e) {
			    fail_out ('出现意外! 把路径 ' + g + ' 重命名为 ' +
				h + ' 时失败! 请手动处理! ' + e);
			    r = f;
			    break label_rename;
			}
			try {
			    fs.renameSync(f, g);
			} catch (e) {
			    fail_out ('出现意外! 把路径 ' + f + ' 重命名为 ' +
				g + ' 时失败! 请手动处理! ' + e);
			    r = f;
			    break label_rename;
			}
		}
		if (fattr == 2)
		    button.value += ' — 文件夹成功接收';
		else if (s_size) {
		    button.value =
			'断点续传已完成, 若有错误, 可点击此按钮重新接收';
		    button.disabled = false; // 重新启用按钮
		} else {
		    button.value = '已接收 100%';
		}

		show_buttons_open_file (r);
	} // rename_files ()

	if (fs.existsSync(f)) {
		let s = fs.statSync(f);
		// 如果不是普通文件, 或者文件长度太小或太大, 则重命名它
		if (fattr == 2 || ! s.isFile() ||
			s.size < 0x1000 || s.size > fsize) {
		    let g; // = f + '.no_use';
//		    if (true || s.isDirectory()) { // 无论目录还是文件都执行
			// 若文件夹是新的, 则退出
			if (fattr == 2 || s.isDirectory()) {
			    let t = Date.now() - s.mtime.getTime();
			    if (t < 30000 && t > -5000) {
				button.value =
				    '同名文件夹已在接收中, 不再重复接收!';
				//button.disabled = true; // 接收按钮保持禁用
				return; // 退出
			    }
			}

			// 先创建临时目录, 再删除它, 从而保证这个名字是
			// 不存在的文件名或目录名
			try {
			    g = fs.mkdtempSync(f + '.no_use-');
			} catch (e) {
			    fail_out ('创建临时目录 ' + f
				+ '.no_use-* 时失败, 无法接收文件! 出错信息: '
				+ e);
			    button.value =
				'创建临时目录时失败! 可点击此按钮重新接收';
			    button.disabled = false; // 重新启用按钮
			    return; // 退出
			}
			// Windows 下需要先删除 g 才能重命名为 g
			try {
			    fs.rmdirSync(g);
			} catch (e) {
			    fail_out ('删除临时目录 ' + g
				+ ' 时失败, 无法接收文件! 出错信息: ' + e);
			    button.value =
				'删除临时目录时失败! 可点击此按钮重新接收';
			    button.disabled = false; // 重新启用按钮
			    return; // 退出
			}
//		    }
		    try {
			fs.renameSync(f, g);
		    } catch (e) {
			fail_out ('把路径 ' + f + ' 重命名为 ' + g +
				' 时失败, 无法接收文件! 出错信息: ' + e);
			button.value = '重命名时出错! 可点击此按钮重新接收';
			button.disabled = false; // 重新启用按钮
			return; // 退出
		    }
		    if (fs.existsSync(f)) {
			fail_out ('把路径 ' + f + ' 重命名为 ' + g +
				' 时出现意外, 无法接收文件!');
			button.value = '重命名未成功! 可点击此按钮重新接收';
			button.disabled = false; // 重新启用按钮
			return; // 退出
		    }
		} else { // 普通文件, 执行断点续传
		    file_exists = true;

		    // 检测是否可以续传
		    if (fs.existsSync(log)) {
			let stat = fs.statSync(log);
			let t = Math.floor(stat.mtime.getTime()/1000)&(-2);
			if (t == (time & (-2)))
			    s_size = s.size;
		    } else { // 不存在 log 文件, 创建它
			try {
			    fs.truncateSync(f);
			} catch (e) {
			    fail_out ('截断文件时失败, 无法接收文件。' + e);
			    button.disabled = false; // 重新启用按钮
			    //dir_sync_start(); // 无论接收成功与否, 都检查
			    return; // 退出
			}
			try {
			    fs.appendFileSync(log, '');
			} catch (e) {
			    fail_out ('创建 log 文件 ' + log +
				' 时出现意外, 无法接收文件! ' + e);
			    button.disabled = false; // 重新启用按钮
			    //dir_sync_start(); // 无论接收成功与否, 都检查
			    return;
			}

			// 设定 log 文件的 mtime 为 time, 也设定 atime 为此值
			try {
			    fs.utimesSync(log, (time&(-2)), (time&(-2)));
			} catch (e) {
			    fail_out (' 设定 log 文件 ' + log +
				' 的 mtime 时出现意外, 无法接收文件! ' + e);
			    button.disabled = false; // 重新启用按钮
			    //dir_sync_start(); // 无论接收成功与否, 都检查
			    return;
			}
		    }


		    // 若文件已全部接收, 则执行重命名
		    if (s_size == fsize) {
			rename_files();
			//dir_sync_start(); // 无论接收成功与否, 都检查
			return;
		    }
		    // 若文件是新的, 则退出
		    let t = Date.now() - s.mtime.getTime();
		    if (t < 30000 && t > -5000) {
			button.value =
				'同名文件已在接收中, 不再重复接收!';
			//button.disabled = true; // 接收按钮保持禁用
			//dir_sync_start(); // 无论接收成功与否, 都检查
			return;
		    }

		    if (isMiao) { // 对方是 miao
			udp_get_file();  // 用 udp 协议接收文件
			return;
		    }
		    if (! navigator.onLine)
		    {
			button.value =
				'没有网络连接, 无法接收! 可点击此按钮重新接收';
			button.disabled = false; // 重新启用按钮
			//dir_sync_start(); // 无论接收成功与否, 都检查
			return;
		    }
		    fileWriteStream = fs.createWriteStream(f,
			{ flags: 'r+', start: s_size });
		}
	}
	if (! file_exists) {
	    if (s_size == fsize && fattr != 2) {
		rename_files();
		//dir_sync_start(); // 无论接收成功与否, 都检查
		return;
	    }
	    // 如果不存在 log 文件, 创建它
	    try {
		fs.appendFileSync(log, '');
	    } catch (e) {
		fail_out ('创建 log 文件 ' + log +
			' 时出现意外, 无法接收文件! ' + e);
		button.disabled = false; // 重新启用按钮
		//dir_sync_start(); // 无论接收成功与否, 都检查
		return;
	    }

	    // 设定 log 文件的 mtime 为 time, 也设定 atime 为此值。
	    try {
		fs.utimesSync(log, (time&(-2)), (time&(-2)));
	    } catch (e) {
		fail_out ('设定 log 文件 ' + log +
			' 的 mtime 时出现意外, 无法接收文件! ' + e);
		button.disabled = false; // 重新启用按钮
		//dir_sync_start(); // 无论接收成功与否, 都检查
		return;
	    }

	    if (isMiao) { // 对方是 miao
		udp_get_file();  // 用 udp 协议接收文件
		return;
	    }
	    if (! navigator.onLine)
	    {
		button.value = '没有网络连接, 无法接收! 可点击此按钮重新接收';
		button.disabled = false; // 重新启用按钮
		//dir_sync_start(); // 无论接收成功与否, 都检查
		return;
	    }
	    fileWriteStream = fs.createWriteStream(f);
	}
	fileWriteStream.on('error', (err) => {
		fail_out ('无法接收来自 ' + remoteAddr + ':' + remotePort +
		    ' 的文件！错误信息: ' + err +
		    '<br/>一般来说, 失败原因有两种, 其一是本地文件' +
		    '系统问题，其二是网络问题。请确保接收文件的设备有足够' +
		    '的剩余空间; 确保接收文件的磁盘支持存放 4G 以上大文件' +
		    '（比如, 不可以是 FAT32 的文件系统类型）; 确保您有权' +
		    '创建文件; 确保物理网络连接正常; 确保文件的发送者没有' +
		    '被他自己的路由器或防火墙阻挡; 确保您的路由器或防火墙' +
		    '没有阻挡文件的发送者。');
		if (fattr == 2) // 接收文件夹
		    button.value = '已接收 ' + fileWriteStream.bytesWritten
				+ ' 字节 —— 接收未完成, 可点击此按钮重新接收';
		else // 接收普通文件
		    button.value = '已接收 ' + (parseInt((fileWriteStream
				.bytesWritten / fsize * 100000)) / 1000)
				+ '% —— 接收未完成, 可点击此按钮重新接收';
		button.disabled = false; // 重新启用按钮
		fileWriteStream.end();
		//fileWriteStream.destroy();
	});
	
	let tcp_client = require('net').Socket();
	let remoteAddr;
	let remotePort;

	tcp_client.setTimeout(30000, () => {
	    tcp_client.end ();
	});

	tcp_client.on ('error', (err) => {
	    fail_out('与 tcp 服务器 ' + remoteAddr + ':' + remotePort +
		' 的连接出错: ' + err);
	});

	tcp_client.on ('close', (had_err) => {
//	    fail_out('已关闭 ' +remoteAddr+ ':' +remotePort+ ' 的 tcp 连接, '
//		+ (had_err ? '有错误发生' : '一切正常'));
	});

	tcp_client.on ('end', () => {
//	    fail_out('已断开 ' +remoteAddr+ ':' +remotePort+ ' 的 tcp 连接');
	});

	let dataSize = 0;
	tcp_client.on ('data', (data) => {
	    let possible_end_size = 0;
	    if (data.length == 0) {
		fail_out('警告！不该收到来自 ' + remoteAddr + ':' +
			remotePort + ' 的长度为 0 的 tcp （空）数据包!');
		return;
	    }
	    dataSize += data.length;
	    if (fattr == 2) // 接收文件夹
		button.value = '已接收 ' + (dataSize + s_size) + ' 字节';
	    else if (fsize > 0)
		button.value = '已接收 ' + (parseInt(
			((dataSize + s_size)/ fsize * 100000)) / 1000) + '%';
	    if (fattr == 2) {
	    	if (dataSize > fsize && data[data.length - 1] == 0x3A) {
		    // 取末尾 50 个字符, 模糊判断数据结束
		    let s = data.slice(-50).toString('utf8').split(':');
		    let len = s.length;
		    if (len < 7)
			return;
		    if (s[len - 4] != 3)
			return;
		    if (s[len - 5] != 0)
			return;
		    if (s[len - 6] != '.')
			return;
		    if (! /^\d+=[0-9a-f]+$/.test(s[len - 3]))
			return;
		    if (! /^\d+=[0-9a-f]+$/.test(s[len - 2]))
			return;
		    //let h = s[len - 7].slice(-4);
		    //let t = parseInt(h, 16);
		    //if (isNaN(t) || t < 8 || t > 50)
			//return;
		    //if (data[data.length + 4 - t] != 0x3A
			//    || data[data.length + 5 - t] != 0x2E)
			//return;
		    possible_end_size = dataSize;
		    setTimeout ( () => {
			if (possible_end_size != dataSize)
			    return;
			if (dataSize != fileWriteStream.bytesWritten)
			    return;
			tcp_client.end ();
		    }, 500);
		    //tcp_client.end ();
		    //fileWriteStream.end();
		    //tcp_client.destroy();
		    //fileWriteStream.destroy();
		}
	    } else if (dataSize >= fsize) {
		tcp_client.end ();
		return;
	    }
	});

	// 建立连接, 从 s_size 开始接收文件 (断点续传)
	if (GATEWAYs && GATEWAYs.split(',').indexOf(Pal[id].IP) >= 0) {
	    fail_out('对方 ' + Pal[id].nick + '(' + id +
		') 的飞鸽使用了经过网关周转的 IP 地址 ' + Pal[id].IP + ':' +
		Pal[id].port + ' 来发送文件, 但这通常会失败!');
	}
	// 若端口不是 2425, 或者 IP 是网关的 ip 地址，则 TCP 协议的接收会失败!
	tcp_client.connect (Pal[id].port, Pal[id].IP, () => {
	    remoteAddr = tcp_client.remoteAddress;
	    remotePort = tcp_client.remotePort;
	    //fail_out ('已连接 tcp 服务器 ' + remoteAddr + ':' + remotePort);
	    let s = '1_lbt6_0#131200#' + stored_randomID +
		'#0#0#0#4001#9#:' // 最后一个 # 号用于测试飞秋的适应性
		+ (SN+1) + ':' + login_name + ':' + me.host + ':' +
		(((fattr == 2) ? IPMSG_GETDIRFILES : IPMSG_GETFILEDATA)
		    | IPMSG_CAPUTF8OPT | IPMSG_UTF8OPT)
		+ ':' + SN.toString(16)
		+ ':' + fno.toString(16) + ':' +
		s_size.toString(16) + ':';
	    // login_name 和 me.host 可能是中文，须转换字符集编码
	    if (charset != 'utf8')
		s = Buffer.from (iconv.encode (s, charset));
	    tcp_client.write (s);
	    if (fsize <= 0 && fattr != 2)
		tcp_client.end ();
	});

	fileWriteStream.on('close', function() {
		//fail_out ('tcp 已成功接收来自 ' + remoteAddr + ':' +
		//    remotePort + ' 的文件。共保存字节数: ' +
		//    fileWriteStream.bytesWritten);
		tcp_client.end ();
		if (fsize <= 0 && fattr != 2) return;
		if (fattr == 2) {
		    button.value += ' - 数据接收完成, 正在处理...';
		    create_dir_files();
		    return;
		}
		if (fileWriteStream.bytesWritten + s_size != fsize) {
		    button.value = '已接收 ' + (parseInt(
		    ((fileWriteStream.bytesWritten + s_size) / fsize
		    * 100000)) / 1000)
		    + '% —— 接收出现异常, 可重新接收';
		    button.disabled = false; // 重新启用按钮
		} else {
		    //button.value = '已接收 100%';
		    rename_files();
		}
	});

	tcp_client.pipe(fileWriteStream);

    } // tcpRecvFile () 函数定义结束

    const myNameSpan = '<span style="background-color:#7FDF7F;color:#802080">';
    const myTextSpan = '</span><span style="color:green"> ';
    //let Url = /\b(https?|ftp|smb|file):\/\/[\w./!~;?:@&=+$,%#-]+/gi;
    const Url = /\b(https?|davs?|webdavs?|ftp|smb|file):\/\/[\w./\[\]!~;?:@&=+$,%#-]+/gi;
    let	auto_recvfiles;
//  let	enable_sync_directory;
    let	imap_secure;
    let	imap_rejectunauth;
    let	smtp_secure;
    let	smtp_rejectunauth;
    let maildebug;

    // 函数定义，打开聊天窗口
    function openChat (msg) {
	let id = (typeof (msg) === 'object') ? msg.id : msg;
	if (Pal[id] == undefined) {
	    //updateTable();
	    return;
	}
	
	let tmpURL = Pal[id].chatwin ? '' : window.location.pathname;
	
	let chatWin;
	let new_win;
	let gain_focus;
	if (typeof (msg) === 'object') {
	    gain_focus = (msg.mainbody.slice(0, 1) != '\0');
	} else {
	    gain_focus = true;
	}
//	if (typeof (msg) !== 'object')
//	    gain_focus = true;
	
	function chatWin_onload () {
	    if (typeof chatWin !== 'object')
		return;
	    if (chatWin.closed) {
		alert ('Fatal: Failed opening window for ' + id);
		Pal[id].chatwin = 0;
		updateTable(id);
		if (typeof nw === 'object') {	// nwjs 的情况
		    if (typeof new_win === 'object' &&
			typeof new_win.close === 'function')
			    new_win.close(true);
		} else {			// electron 的情况
		    require('electron').ipcRenderer.send('quit-guest', id);
		}
		return;
	    }
	
 	    if (typeof nw === 'object') {	// nwjs 的情况
		if (new_win.width < 1040)
		    new_win.width = 1040;
		if (new_win.height < 600)
		    new_win.height = 600;
		new_win.title = Pal[id].nick + ' (' + id + ')';
		if (gain_focus) {
		    new_win.show ();
		    //new_win.focus ();
		}
	    } else {				// electron 的情况
		chatWin.document.title = Pal[id].nick + ' (' + id + ')';
		if (gain_focus)
		    require('electron').ipcRenderer.send('show-guest', id);
	    }

	    //Pal[id].chatwin = chatWin;
	    //if (Pal[id].chatwin) { alert(Pal[id].chatwin.chat_var_test); }
	    updateTable(id);

	    let div_out;
	    let div_in;
	    let div_text;
	    let div_file;
	    //let textin;
	    let p;
	    let button_mail;
	    let checkbox_cc;
	    let cc_id;
	    let cc_id_index = 0;
	    let button_send;
	    let pal_old_time;
	    let button_send_time;
	    let button_send_count = 0;
	    let filesToSend;
	    let SendFileStateChanged = false;
	    //let send_done = false;
	    let backup_send_SN;
	    let input;
	    let button_clear;

	    let fail_out = function (str) { fail (str, chatWin); };

	    function send_to_next () {
		button_send_count = 0;
		//send_done = true;
		if (checkbox_cc.checked) {
		    cc_id_index ++;
		    if (cc_id_index < cc_id.length) {
			//send_done = false;
			setTimeout(send_to, 50, (cc_id[cc_id_index]));
		    } else {
			checkbox_cc.checked = false;
		    }
		}
	    }

	    function send_to (Id) {

		// 若对方在内网，发消息前，先探测其是否在线

		// 若对方罩在路由器下，而且不是 DMZ 主机，必须先发送
		// 探测消息激活它。探测消息是发往 DMZ 主机的。DMZ 主机
		// 收到后，会转播给路由器下的每台主机。目的主机收到转
		// 播的消息后，立即回复你一条消息，于是通道得以建立。

		// 你看到的他的ip与你处于同一局域网，但端口号不是2425,
		// 这就说明，他是罩在路由器下。

		// let oldtime = Pal[Id].time;
		let isIPv4 = ! /:/.test(Pal[Id].IP);

//		if (isIPv4 && Pal[Id].haskey &&
//		    Pal[Id].myIPs && Pal[Id].mediacy &&
//			(Pal[Id].port != 2425 ||
//			 Pal[Id].myIPs.split(',').indexOf(Pal[Id].IP) == -1))
//		{
//			// 如果是经由中介服务器转播，首先向中介发送
//			// 转播探测包，中介会转发给对方。与此同时也
//			// 直接发送探测包给对方，打开回程通路。对方
//			// 收到后会回应，表明连接成功。为了确保连接
//			// 成功，再发送一个直接探测包，对方若正确回
//			// 应，则确认连接成功。
//			// sendMsg(me, 中介, IPMSG_BR_ENTRY, me.nick);
//			let ip_port = Pal[Id].mediacy.split(':');
//
//			if (! checkCIDR(ip_port[0])) {
//			    // 中介不在内网。
//			    // 向中介发送上线消息，并请求转发给每个
//			    // 在线客户。中介会给你发别人上线消息，
//			    // 同时也给别人发你上线的消息。这样，你
//			    // 与所有的在线客户都连通了。
//			    window.document.getElementById
//					('detectMediator').onclick();
//			} else {
//			    //var ready = false;
//			    for (var i = 0; i < 10; i++) {
//				// 先向 2425 端口发探测消息
//				let port_bak = Pal[Id].port;
//				Pal[Id].port = 2425;
//				sendMsg(me, Pal[Id], IPMSG_BR_ENTRY,
//					me.nick);
//				Pal[Id].port = port_bak;
//				// 再向实际端口发探测消息
//				//sendMsg(me, Pal[Id], IPMSG_BR_ENTRY,
//				//	me.nick);
//				//if (oldtime != Pal[Id].time) {
//				//    ready = true;
//	   			//    break;
//				//}
//	   			// 休息一下
//	   			let tmp_time = Date.now();
//	   			while (Date.now() - tmp_time < 50);
//			    }
//			    //if (! ready) {
//				//chatWin.alert
//				//    ('对方不在线，无法发送消息。');
//				//return;
//			    //}
//			}
//		}

		if (button_send_count == 0) {
		    pal_old_time = Pal[Id].time;
		    button_send_time = Date.now();
		}
		if (button_send_count > (isIPv4 ? 200 : 600) ||
			Date.now() - button_send_time >
				(isIPv4 ? 10000 : 60000)) {
		    Pal[Id].offline = 1; // 标记好友已离线
	    	    updateTable(Id); // 用户列表更新离线信息
		    if (cc_id == null) {
			    // 隐藏发送按钮
			    //button_send.style.display = 'none';
			    button_send.innerText = '发送（对方可能已离线）';
		    }
		    fail_out(
			    Pal[Id].nick + '(' + Id + ') 不在线，无法发消息。'
			    + '<br/>对方可能已离线、关机、休眠, 或 IP 已变。'
			    + '<br/>温馨提示: 同一个好友'
			    + '可能同时拥有多个 IP, 您可尝试给好友的'
			    + '其它 IP 发消息。<br/><br/>'
			    + '常见的错误是: 您（或对方）切换了 WiFi 信号, '
			    + '导致原先的用户 IP 失效。<br/>'
			    + '本聊天窗可能是在与已经失效的用户 IP 进行对话。'
			    + '请关闭此聊天窗, 刷新好友列表, 找到好友的新 IP, '
			    + '然后重新建立对话。<br/>'
		    );
		    send_to_next ();
		    return;
		}
		if (pal_old_time == Pal[Id].time && stored_randomID !=
			Id.replace(/@.*$/,'').replace(/:/g,'_')) {
		    // 没有收到应答, 暂不发送消息，继续探测
		    if (cc_id == null)
			button_send.innerText = '发送';

		    // let ready = false;
		    // HiWiFi 路由器的 bug，需要重复发多次才能唤醒
		    // HiWiFi 路由器最初几次会让目的机使用 2425 外网
		    // 端口回复你，这是错误的，因为 2425 端口是由 DMZ
		    // 主机来使用的。多次探测之后，才会回归正确端口号
		    if (isIPv4)
		    if (button_send_count < 160 &&
				! (button_send_count & 7)) {
			let port_bak = Pal[Id].port;
			// 先向 2425 端口发探测消息
			// 注：路由器的 DMZ 主机会转播此探测消息
			Pal[Id].port = 2425;
			sendMsg(me, Pal[Id], IPMSG_BR_ENTRY, me.nick);
			if (port_bak != 2425) {
			    Pal[Id].port = port_bak;
			    // 再向实际端口发探测消息
			    setTimeout(sendMsg, 320, me, Pal[Id],
					IPMSG_BR_ENTRY, me.nick);
			}
		    }
		    if (! isIPv4)
		    if (button_send_count == 0) {
			sendMsg(me, Pal[Id], IPMSG_BR_ENTRY, me.nick);
		    }
		    if (! isIPv4 && button_send_count && miaoconfig[Id] &&
			miaoconfig[Id].GUAsCache)
		    if ((button_send_count & 7) == 0) {
			let keys = Object.keys(miaoconfig[Id].GUAsCache);
			let arr = Object.keys(obj);
			for (let i = 0; i < keys.length; i++) {
			    if ((button_send_count >>> 3) - 1 == i) {
				who.IP = keys[i];
				who.port = 2425;
				sendMsg(me, who, IPMSG_BR_ENTRY, me.nick);
			    }
			}
		    }
	   	    // 短暂停留 5 毫秒, 异步调用本函数
		    //setTimeout(button_send.onclick, 5);
		    setTimeout(send_to, 50, Id);
		    button_send_count ++;
		    return; // 本次执行, 只探测, 不发消息, 立即返回!
		}

		send_to_next ();

		let send_plain = false;
		if (Pal[Id].haskey != 1)
			send_plain = true;
		if (! effective_mailuser || ! effective_mailpass) {
			// 自己无邮箱，不给别人发送密文
			send_plain = true;
		}
		let account = Pal[Id].version.split('#')[8];
		if (! account) {
			// 对方无邮箱，不向他发送密文
			send_plain = true;
		}
		if (! /@/.test(account))
		    account += '@ethereal.email';
		// 查询是否为好友
		tmp = stored_friends.split(/\s[^\r\n]*[\r\n]{0,}/);
		if (tmp.indexOf(account) == -1) {
			// 对方不是好友，不向他发送密文
			send_plain = true;
		}
		// 是否已获取对方的公钥
		let keyName = 'publicKey' + Id.slice(0,16);
		let keyStr = miaoconfig[keyName];
		if (! keyStr)
			send_plain = true;

		let sendString = div_text.innerText;

		// 更新要发送的内容, 附加要发送的文件信息
		if (SendFileStateChanged) {
		    let count = Pal[id].send.input.files.length;
		    let j = 0;
		    for (let i = 0; i < count; i++) {
			let f = Pal[id].send.input.files[i];
			if (f.sent)	// 发送过的文件
			    continue;	// 不再发送
			if (j == 0) sendString += '\0';
			j++;
			let name = f.name;
			// 若给 miao 发送同步文件, name 包含相对路径
			if (Pal[Id].haskey) {
			    let palDirName = Pal[Id].nick +
				Id.replace(/@.*$/,'').replace(/:/g,'_');
			    let d = path.resolve('..', 'shared', palDirName,
						'sync') + path.sep;
			    let p = path.resolve(f.path);
			    if (p.indexOf(d) == 0) {
				name = p.slice(d.length - 5); // 'sync/...'
			    }
			}
			// 冒号是非法Windows文件名字符，避免使用它
			// 编号从 1 开始
			sendString += (i+1) + ':' + name.replace(/:/g, '_')
			    + ':' + f.size.toString(16) + ':' +
			    (Math.floor(f.lastModified / 1000))
			    .toString(16) + ':1:\u0007';
		    }
		}
		let bytes = Buffer.byteLength(sendString, 'utf8');
		if (bytes > 0xF000) {
		    fail_out('消息太长(=' + bytes + '字节), 拒绝发送！');
		    return;
		}
		let t = sendString.split('\0');

		function sendstr () {
		    send_SN = backup_send_SN;

		    let cmd = IPMSG_SENDMSG;
		    let backup_file_send_SN = chatWin.file_send_SN;
		    // 检查消息尾部是否含有文件信息
		    if (t.length > 1 &&
			t[1].replace (/\d+:.*$/, '') == '' &&
			t[1].slice(-2) == ':\u0007') {
			// 发送带附件的消息
			cmd |= IPMSG_FILEATTACHOPT |
				IPMSG_READCHECKOPT |
				IPMSG_SENDCHECKOPT |
				IPMSG_SECRETOPT;
			if (! chatWin.file_send_SN)
			    chatWin.file_send_SN = backup_send_SN;
			send_SN = chatWin.file_send_SN;
		    }
		    // 多个文件正在发送
		    // sending 表示开始发送文件
		    // 发送前, sending = -(待发送文件总数)
		    // 某文件开始发送, 先让 sending >= 0; 然后 sending ++
		    // 某文件发送完成, 在 sending > 0 时执行 sending --
		    // 外部代码可以检查 sending 的值, 如果为 0, 表示发送完成,
		    // 如果为负数, 表示尚未发送任何文件, 如果为正数,
		    // 表示有文件正在发送。
		    if (! Pal[Id].port || ! Pal[Id].IP) {
			chatWin.file_send_SN = backup_file_send_SN;
			fail_out('对方是 Email 好友, 没有局域网 IP 地址, ' +
				'不能直接发消息。咱可点击【邮寄】按钮。');
			return;
		    }
		    const sn = send_SN; // 这是实际发送的消息序号
		    cmd |= IPMSG_SENDCHECKOPT; //请求对方反馈一条“收讫”消息
		    if (! sendMsg(me, Id, cmd, sendString)) {
			chatWin.file_send_SN = backup_file_send_SN;
			fail_out('消息未发出！可能因消息太长或网络故障。');
			return;
		    }

		    // 发送成功
		    send_SN = backup_send_SN + 1;
		    if (SendFileStateChanged) {
			Pal[id].send.input.ownerDocument = chatWin.document;
			let files = Pal[id].send.input.files;
			//// 清除已发送的文件项
			//while ( true ) {
			//    if (! files.length)
			//	break;
			//    if (! files[0].sent)
			//	break;
			//    files.shift();
			//}

			// 标记未发送的文件项为已发送
			let c = Pal[id].send.input.files.length;
			// chatWin.sending = -c;
			for (let i = 0; i < c ; i++) {
			    let f = Pal[id].send.input.files[i];
			    if (! f.sent)
				f.sent = true;
			}
			if (! Pal[Id].send)
			    Pal[Id].send = {};
			Pal[Id].send.input = Pal[id].send.input;
	 		button_clear.style.display = 'none';
			SendFileStateChanged = false;
		    }

		    let r = '';
		    if ((cmd & IPMSG_FILEATTACHOPT) && t[1] && t[1] != '') {
			let s = t[1].split('\u0007');
			for (let i in s) {
			    let f = s[i].split(':');
			    let n = f.length - 1; // 最后一项
			    //if (f[0] != i)
			    //	continue; // 文件序号不正确
			    if (f[n] == undefined || f[n] != '')
				continue; // 结束位置不正确
			    if (f[4] != 1 && f[4] != 2)
				continue; // 文件类型不正确
			    r += f[1] + ((f[4]==1)?'【文件】' +
				parseInt(f[2], 16) + ' 字节<br/>'
				: '〖文件夹〗（暂不支持发送）<br/>');
			}
			r = '<br/>已推送 ' + (s.length - 1) +
			    ' 个附件:<br/>' + r;
		    }

		    let tt = t[0]
			.replace(/^\x02[^\s\0-/:-@[-^\x60\x7b-\x7f]+\x02/, '')
			.replace(/</g, '&lt;')
			.replace(/>/g, '&gt;')
			.replace(/\s*\n/g, '<br/>')
			.replace(Url, function (match) {
			    return '<input type="button" value="'
				+ match + '" title="' +
				'点击按钮将调用系统默认浏览器打开此网址" ' +
				'onclick="window.opener.openExternal('
				+ "'" + match + "'" + ');"/>';
			})
			+ r;
		    if (! tt) { // Rexec 口令验证的情形
			div_text.innerText = '';
			div_text.focus();
			return;
		    }
		    let	p = chatWin.document.createElement('p');
		    let prompt = (Pal[Id].haskey && sendString.slice(0,24) ==
			    '\0cipher\0' + Id.slice(0,16)) ? '#' : ':';
		    if (cc_id == null)
			p.innerHTML = myNameSpan + (new Date())
			    .toLocaleString (date_locale, date_options) + ' '
			    + me.nick + prompt + myTextSpan + tt + '</span>';
		    else if (checkbox_cc.checked)
			p.innerHTML = myNameSpan + (new Date())
			    .toLocaleString (date_locale, date_options) + ' '
			    + me.nick + prompt + myTextSpan + '➤ ' +
			    Pal[Id].nick + '【' + Id +'】' + '</span>';
		    else
			p.innerHTML = myNameSpan + (new Date())
			    .toLocaleString (date_locale, date_options) + ' '
			    + me.nick + prompt + myTextSpan + '➤ ' +
			    Pal[Id].nick + '【' + Id +'】<br/>' + tt
			    + '</span>';
 		    //p.style.color = 'blue';
		    p.style.margin = 0;
		    //p.style.padding = 0;
		    div_out.appendChild(p);
		    p.scrollIntoView(false);
		    save_history(p.innerHTML, Id);
		    if (checkbox_cc.checked)
			return;
		    //textin.value = '';
		    //textin.focus();
		    p.lastElementChild.style.backgroundColor = 'cyan';
		    p.lastElementChild.title = '此消息未送达';
		    div_text.innerText = '';
		    div_text.focus();

		    // 等待 backup_send_SN 的收讫通知

		    // 只保留 200 个元素
		    let c;
		    while ((c = Object.keys(RecvMsgCBs).length) > 199) {
			delete RecvMsgCBs[Object.keys(RecvMsgCBs)[0]];
			if (c <= Object.keys(RecvMsgCBs).length) {
			    // 万一出现奇葩走到此处
			    p.lastElementChild.style.backgroundColor =
					'yellow';
			    p.lastElementChild.title =
				'出现意外：无法检测此消息是否送达';
			    return;
			}
		    }
		    RecvMsgCBs[sn] = [];
		    RecvMsgCBs[sn].push(function () {
			p.lastElementChild.style.backgroundColor =
				'transparent';
			p.lastElementChild.title = '';
		    });
		    RecvMsgCBs[sn].push(Date.now());

		} // 局部函数 sendstr () 结束

		if (send_plain) { // 发送明文
		    sendstr ();
		    return;
		}
		// 发送密文
		let keyObj = JSON.parse(keyStr);
		let publicKey;
		window.crypto.subtle.importKey ('jwk', keyObj,
			{ name: 'RSA-OAEP', hash: 'SHA-256' },
			true, ['encrypt'])
		.then (function (keydata) {
		    publicKey = keydata;
		    let enc = new TextEncoder ();
		    let encoded = enc.encode(sendString);
		    let cypher;
		    let j = 0;
		    for (let i = 0; i < encoded.length; i += 446) {
			window.crypto.subtle.encrypt (
				{ name: 'RSA-OAEP' },
				publicKey,
				encoded.slice(i, i + 446))
			.then (function(cipher) {
			    j += 446;
			    // XP 修复! Buffer.from 不能作用于 ArrayBuffer
			    // object, 须包装成 Uint8Array 才行
			    let buf = Buffer.from(new Uint8Array(cipher));
			    if (cypher)
				cypher = Buffer.concat([cypher, buf]);
			    else
				cypher = Buffer.concat([buf]);
			    if (j < encoded.length)
				return;
			    sendString = '\0cipher\0' + Id.slice(0,16) +
					 bin2text(cypher);
			    if (sendString.length > 0xF000) {
				fail_out('密文太长(=' + sendString.length
					+ '字节), 拒绝发送！');
				return;
			    }
			    sendstr ();
			}).catch (function (err) {
			    fail_out ('用 ' + Id.replace (/@.*$/,'') +
				' 的公钥加密时出错: ' + err +
				' 文本框中的字符数: ' +
				div_text.innerText.length +
				' 占用的字节数: ' +
				encoded.length);
			});
		    }
		}).catch (function (err) {
			fail_out ('导入 ' + Id + ' 的公钥时出错: ' + err);
		});
	    } // send_to (Id) 局部函数定义结束

	    function button_send_onclick () {
		if (! Pal[id].IP || ! Pal[id].port) {
		    fail_out('对方无局域网 IP, 咱只能点击邮寄按钮。');
		    return;
		}
		//imapConnect (); // 检查邮件
		let tmp = div_text.innerText;
		if (tmp.length > 60000) {
		    fail_out(
			'文本超长！文本框中的字符数太多(=' + tmp.length
			+ '个), 已超过 6 万，拒绝发送！<br/>'
			+ '注意，每个汉字可占据多个字节, '
			+ '而且在加密的情况下，需要的字节数又会成倍增加。'
			+ '因此, 最终能够成功发送的字符数大约在 1 万左右。'
			+ '保险一点，请控制在 5000 个字符以内。'
		    );
		    return;
		}
		div_text.innerText = tmp.replace(/(^\s*)|(\s*$)/g,'');

		// 若有文件标记为“未发送”, 这说明文件发送的状态已发生变化,
		// 也即, 已有新文件添加到发送列表中。
		SendFileStateChanged = false;
		if (! button_send.disabled) {
		    let len = Pal[id].send.input.files.length;
		    for (let i = 0; i < len; i++) {
			let f = Pal[id].send.input.files[i];
			if (! f.sent)
			    SendFileStateChanged = true;
		    }
		}
		// 若文本框为空, 且文件发送列表中没有添加新文件, 则不执行发送
		if (div_text.innerText.length <= 0 && (! SendFileStateChanged))
		    return;

		backup_send_SN = send_SN;
		let Id = id;
		cc_id = null;
		if (checkbox_cc.checked) {
		    if (! stored_cc)
			stored_cc = '';
		    let cc = stored_cc.split(/[\r\n]+/); // 检查每一行
		    cc_id = [];
		    for (Id in Pal) {
			for (let i in cc) {
			    if (cc[i].replace (/[\s]+/g, '') == '') {
				break; // 群发设置框中的空白行之后都当作注释
			    }
			    let reg = new RegExp(cc[i], '');
			    if (reg.test(Pal[Id].nick + ':' +
					Pal[Id].group + ':' + Pal[Id].IP)) {
				cc_id.push(Id);
				break;
			    }
			}
		    }
		    if (cc_id.length == 0) {
			fail_out(
			    '您勾选了“群发”，但目前满足条件的接收人数为'
			    + ' 0, 因此不执行发送。<br/>'
			    + '您可以在主窗口的界面中先刷新一下好友, '
			    + '然后再发送; 若有必要, 也可更改群发设置。<br/>'
			    + '注意: 如果群发设置框的第一行为空白, '
			    + '那就表示没有接收者（也即“禁止群发”）。<br/>'
			    + '提示: 在群发设置中, 一般来说, 遇到一个空白行, '
			    + '就会忽略其后所有的行。'
			);
			return;
		    }
		    cc_id_index = 0;
		    Id = cc_id[0];
		}
		//send_done = false;
		send_to (Id);
	    } // button_send_onclick () 局部函数定义结束

	    function button_mail_onclick () {
		if (this.innerText == '加为好友') {
		    if (! effective_mailuser || ! effective_mailpass) {
			fail_out('咱自己无邮箱，无法加对方为好友。');
			this.style.display = 'none'; // 自己无邮箱
			return;
		    }
		    let account = Pal[id].version.split('#')[8];
		    let tmp;
		    if (! account) {
			fail_out('对方无邮箱，无法加为好友。');
 			this.style.display = 'none';
			return;
		    }
		    if (! /@/.test(account))
			account += '@ethereal.email';
		    // 查询是否已经为好友
		    tmp = stored_friends.split(/\s[^\r\n]*[\r\n]{0,}/);
		    if (tmp.indexOf(account) != -1) {
			this.innerText = '邮寄'; // 已是好友
			this.title = '开始邮寄消息和文件。\n\n' +
				'自动跳过已寄出文件, 不重复邮寄。\n\n';
			return;
		    }
		    // 还不是好友，现在添加
		    tmp = account + ' ' +
				Pal[id].version.split('#')[2] + ' ' +
				Pal[id].nick + ' ' + Pal[id].wgroup;
		    if (Pal[id].myIPs && /:/.test(Pal[id].IP))
			tmp += ' ' + Pal[id].IP + ' ' + Pal[id].myIPs;
 		    stored_friends += '\r\n' + tmp;
 		    stored_friends = stored_friends
				.replace (/^[\r\n]+/, '')
				.replace (/(\r\n){2,}/g, '\r\n');
		    miaoconfig.friends = stored_friends;
		    miaoconfig_needupdate = true;
		    update_miaoconfig();
		    updateTable(); // 按钮是手动点击, 因此不会造成无限循环
		    let friends = window.document.getElementById('friends');
		    friends.value = stored_friends;
		    fail_out('好友邮箱、机器码、昵称、工作组是:<br/>'
				+ tmp.replace (/\s+/g, '<br/>'));
		    this.innerText = '邮寄'; // 已是好友，可邮寄
		    this.title = '开始邮寄消息和文件。\n\n' +
				'自动跳过已寄出文件, 不重复邮寄。\n\n';
		    return;
		} else if (this.innerText == '邮寄') {
		    if (! effective_mailuser || ! effective_mailpass) {
			fail_out('咱自己无邮箱，无法邮寄。');
			this.style.display = 'none'; // 隐藏邮寄按钮
			return;
		    }
		    let account = Pal[id].version.split('#')[8];
		    let tmp;
		    if (! account) {
			fail_out('对方无邮箱，无法邮寄。');
			this.style.display = 'none'; // 隐藏邮寄按钮
			return;
		    }
		    if (! /@/.test(account))
			account += '@ethereal.email';
		    // 查询是否已经为好友
		    tmp = stored_friends.split(/\s[^\r\n]*[\r\n]{0,}/);
		    if (tmp.indexOf(account) == -1) {
			this.innerText = '加为好友'; // 不是好友
			return;
		    }
		} else {
		    fail_out('严重错误！按钮不是邮寄、加为好友。');
		    this.style.display = 'none'; // 隐藏按钮
		    return;
		}
		let tmp = div_text.innerText;
		div_text.innerText = tmp.replace(/(^\s*)|(\s*$)/g,'');
		let sendString = div_text.innerText;

		// 只请求获取对方 IP，不发送消息
		if (sendString.toLowerCase() == 'ip') {
		    SendMail(id, 'ASKKEY', 'ip', (err) => {
			div_text.innerText = ''; // 无论是否成功都清除文本
			div_text.focus();
			if (err) {
			    fail_out('邮寄失败, 无法获取对方 IP。' +
				 '若紧接着的出错信息是连接超时, 咱可重试。'
				+ err);
			    return;
			}
			// 邮寄成功, 30 秒后检查邮件
			last_mail_time = Date.now();
			setMailFetchInterval(30000);
		    });
		    return;
		}

		// 若有文件标记为“未发送”, 这说明文件发送的状态已发生变化,
		// 也即, 已有新文件添加到发送列表中。
		SendFileStateChanged = false;
		if (! button_mail.disabled) {
		    let len = Pal[id].send.input.files.length;
		    for (let i = 0; i < len; i++) {
			let f = Pal[id].send.input.files[i];
			if (! f.sent)
			    SendFileStateChanged = true;
		    }
		}
		// 若文本框为空, 且文件发送列表中没有添加新文件, 则不执行发送
		if (div_text.innerText.length <= 0 && (! SendFileStateChanged))
		    return;

		if (! sendString) {
		    fail_out('邮寄时, 消息不可以为空。');
		    return;
		}
		//if (! Pal[id].haskey) {
		//    fail_out('对方不是 miao 用户, 不能邮寄。');
		//    return;
		//}

		if (! effective_mailuser || ! effective_mailpass) {
		    fail_out('咱自己无邮箱，无法邮寄。');
		    return;
		}

		let account = Pal[id].version.split('#')[8];
		let ethereal = /@ethereal\.email$/.test(effective_mailuser);

		if (! account) {
		    fail_out('对方无邮箱，无法邮寄。');
		    return;
		}
		if (! /@/.test(account)) { // 对方是 ethereal 邮箱
		    if (ethereal) { // 自己也是 ethereal 邮箱
			account += '@ethereal.email';
		    } else {
			fail_out('用普通邮箱无法寄往对方 ethereal 邮箱。');
			return;
		    }
		} else { // 对方是普通邮箱
		    if (ethereal) { // 但自己却是 ethereal 邮箱
			fail_out('用 ethereal 邮箱无法寄往对方普通邮箱。');
			return;
		    }
		}

		// 如果前一个邮寄子进程还未结束，复用它会出现混乱
		if (sendMail != null) {
		    fail_out('请等待前一个邮件发完后再邮寄。');
		    return;
		}

		let send_plain = false;

		if (Pal[id].haskey != 1)
		    send_plain = true;

		// 查询是否为好友
		tmp = stored_friends.split(/\s[^\r\n]*[\r\n]{0,}/);
		if (tmp.indexOf(account) == -1) {
		    // 对方不是好友，不向他发送密文
		    send_plain = true;
		}
		if (! chatWin.encryptmail) {
		    // 咱未启用加密，不发送密文
		    send_plain = true;
		}
		// 是否已获取对方的公钥
		let keyName = 'publicKey' + id.slice(0,16);
		let keyStr = miaoconfig[keyName];
		if (! keyStr)
		    send_plain = true;
		else
		    Pal[id].haskey = 1;

		//Pal[id].haskey = ((keyStr + '' == 'null') ? -1 : 1);
		//if (Pal[id].haskey != 1) {
		//    fail_out('没有对方的密钥, 不能邮寄。');
		//    return;
		//}

		let sendFiles = '';

		// 更新要发送的内容, 附加要发送的文件信息
		if (SendFileStateChanged) {
		    let count = Pal[id].send.input.files.length;
		    for (let i = 0; i < count; i++) {
			let f = Pal[id].send.input.files[i];
			if (f.sent)	// 发送过的文件
			    continue;	// 不再发送
			let name; // = f.name;
			// 若给 miao 发送同步文件, name 包含相对路径
			//if (Pal[id].haskey) {
			    let palDirName = Pal[id].nick +
				id.replace(/@.*$/,'').replace(/:/g,'_');
			    let d = path.resolve('..', 'shared', palDirName,
						'sync') + path.sep;
			    let p = path.resolve(f.path);
			    name = p;
			    if (p.indexOf(d) == 0) {
				name = p.slice(d.length - 5); // 'sync/...'
				name = '../shared/' + palDirName + '/' + name;
			    }
			//}
			name = name.replace(/\\/g, '/');
			// 冒号是非法Windows文件名字符，避免使用它
			sendFiles += '\0' + name; //.replace(/:/g, '_');
		    }
		}

		function cb (err) {
		    if (err) {
			fail_out('邮寄失败。若出错信息是连接超时, 请重试。'
				+ err);
			return;
		    }
		    if (SendFileStateChanged) {
			Pal[id].send.input.ownerDocument=chatWin.document;
			let files = Pal[id].send.input.files;

			// 标记未发送的文件项为已发送
			let c = Pal[id].send.input.files.length;
			// chatWin.sending = -c;
			for (let i = 0; i < c ; i++) {
			    let f = Pal[id].send.input.files[i];
			    if (! f.sent)
				f.sent = true;
			}
			button_clear.style.display = 'none';
			SendFileStateChanged = false;
		    }

		    let r = sendFiles.replace(/\0([^\0]+)/g,
				'<br/>已寄出文件 $1');

		    let tt = div_text.innerText
			.replace(/^\x02[^\s\0-/:-@[-^\x60\x7b-\x7f]+\x02/, '')
			.replace(/</g, '&lt;')
			.replace(/>/g, '&gt;')
			.replace(/\s*\n/g, '<br/>')
			.replace(Url, function (match) {
			    return '<input type="button" value="'
				+ match + '" title="' +
				'点击按钮将调用系统默认浏览器打开此网址" ' +
				'onclick="window.opener.openExternal('
				+ "'" + match + "'" + ');"/>';
			})
			+ r;
		    if (tt) {
			// 不是 Rexec 口令验证
			let p = chatWin.document.createElement('p');
			//p.style.color = 'blue';
			p.style.margin = 0;
			//p.style.padding = 0;
			p.innerHTML = myNameSpan + (new Date())
				.toLocaleString(date_locale, date_options)
				+ ' ' + me.nick + (send_plain ? ':' : '#')
				+ myTextSpan + tt + '</span>';
			div_out.appendChild(p);
			p.scrollIntoView(false);
			save_history(p.innerHTML, id);
		    }
		    div_text.innerText = ''; // 清除已经发送的文本
		    div_text.focus();
		    // 邮寄成功, 30 秒后检查邮件
		    last_mail_time = Date.now();
		    setMailFetchInterval(30000);
		}

		if (send_plain) { // 发送明文
		    SendMail(id, 'SENDMSG', sendString + sendFiles, cb);
		    return;
		}

		// 用公钥加密，并发送密文
		let keyObj = JSON.parse(keyStr);
		let publicKey;
		window.crypto.subtle.importKey ('jwk', keyObj,
			{ name: 'RSA-OAEP', hash: 'SHA-256' },
			true, ['encrypt'])
		.then (function (keydata) {
		    publicKey = keydata;
		    let enc = new TextEncoder ();
		    let encoded = enc.encode(div_text.innerText);
		    let cypher;
		    let j = 0;
		    for (let i = 0; i < encoded.length; i += 446) {
			window.crypto.subtle.encrypt ({ name: 'RSA-OAEP' },
			    publicKey, encoded.slice(i, i + 446))
			.then (function(cipher) {
			    j += 446;
			    let buf = Buffer.from(new Uint8Array(cipher));
					     // WinXP 需要用 Uint8Array 过渡
			    if (cypher)
				cypher = Buffer.concat([cypher, buf]);
			    else
				cypher = Buffer.concat([buf]);
			    if (j < encoded.length)
				return;
			    SendMail(id, 'SENDMSG', '\0cipher\0' + id.slice(0,
				16) + bin2text(cypher) + sendFiles, cb);
			}).catch (function (err) {
			    fail_out('用 ' + id +' 的公钥加密时出错: ' + err);
			});
		    }
		}).catch (function (err) {
			fail_out('导入 ' + id +	' 的公钥时出错: ' + err);
		});
 		return;
	    } // button_mail_onclick () 局部函数定义结束

	    function create_in_out_boxes () {
		div_out = chatWin.document.createElement('div');
		div_out.id = 'outputbox';
		div_out.style.overflow = 'scroll';
		div_out.style.position = 'absolute';
		div_out.style.wordWrap = 'break-word';
		div_out.style.top = 0;
		div_out.style.left = 0;
		div_out.style.right = 0;
		div_out.style.bottom = '30%';
		//div_out.style.height = '70%';
		//div_out.style.width = '100%';
		//div_out.style.maxHeight = '400px';
		//div_out.style.minHeight = '100px';
		chatWin.document.body.appendChild(div_out);
		div_in = chatWin.document.createElement('div');
		div_in.id = 'inputbox';
		//div_in.style.overflow = 'scroll';
		div_in.style.position = 'absolute';
		div_in.style.bottom = 0;
		div_in.style.top = '70%';
		//div_in.style.maxHeight = '600px';
		//div_in.style.minHeight = '100px';
		//div_in.contentEditable = true;
		chatWin.document.body.ondragover = function (e) {
		    e.preventDefault();
		};
		function do_text_and_files (data, e, drop) {
		    if (! data)
			return;
		    if (! data.getData)
			return;
		    if (! data.files)
			return;
		    let text = data.getData('text');

		    label_text:
		    if (text) {
			//break label_text;
			if (div_text.contains(e.target))
			    break label_text; // 不执行 e.preventDefault()
			e.preventDefault();
			// 目标在 div_text 之外，只能手动执行粘贴
			let selection = chatWin.getSelection();
			if (! selection || ! selection.rangeCount) {
			    div_text.focus(); //激活 caret, 使 selection 有效
			    selection = chatWin.getSelection();
			    if (! selection || ! selection.rangeCount) {
				fail ('文本框未设定插入点, 无法粘贴文本。');
				break label_text;
			    }
			}
			let range = selection.getRangeAt(0);
			if (/^\s*$/.test(div_text.innerText)) {
			    div_text.innerText = text;
			    range.selectNodeContents(div_text);
			    break label_text;
			}
			//if (! selection || ! selection.rangeCount) {
			//    div_text.innerText = text + div_text.innerText;
			//    break label_text;
			//}

			range.deleteContents(); // 将 range 的内容清空
			//若把 div 换成 textarea, 粘贴的内容含 textarea 标签
			let div = chatWin.document.createElement('div');
			div.innerText = text;
			range.insertNode(div);

			//下面这个方法, 粘贴的内容丢失了回车换行
			//range.deleteContents(); // 将 range 的内容清空
			//let textnode = chatWin.document.createTextNode(text);
			//range.insertNode(textnode);

			//下面这个方法, 未能选中粘贴的内容
			//let node_a = range.startContainer;
			//let offset_a = range.startOffset;
			//let range_a = chatWin.document.createRange();
			//range_a.setStartBefore(div_text.firstChild);
			//range_a.setEnd(node_a, offset_a);
			//let node_b = range.endContainer;
			//let offset_b = range.endOffset;
			//let range_b = chatWin.document.createRange();
			//range_b.setStart(node_b, offset_b);
			//range_b.setEndAfter(div_text.lastChild);
			//let frag_b = range_b.extractContents();
			//let frag = range.extractContents();
			//let frag_a = range_a.extractContents();
			//// 以上三条语句清空 div_text
			//div_text.innerText = text;
			//range = chatWin.document.createRange();
			//range.selectNodeContents(div_text);
			//frag = range.extractContents(); // 清空 div_text
			//div_text.appendChild(frag_a);
			//div_text.appendChild(frag);
			//div_text.appendChild(frag_b);

			break label_text; // 这句用来屏蔽以下测试代码

			//selection.addRange(range);

fail(text);
			if (drop) {
			    if (div_text.ondrop)
				div_text.ondrop(e);
			} else {
fail(div_text);
	setTimeout(()=>{
			    let ttt;
			let evt = new Event('drop');
fail('==========');
			try {
				evt.dataTransfer = {
					getData:	function () {
						return text;
					}
				}
			} catch (ex) {
fail(ex);
			}
		div_text.focus();
fail(evt.dataTransfer.getData('text'));
			try {
				ttt = div_text.dispatchEvent(e);
			} catch (ex) {
fail(ex);
			}
fail(ttt);
	}, 100);
			//    if (div_text.onpaste)
			//	div_text.onpaste(e);
			}

		    } else {
			e.preventDefault();
		    }

		    let c = data.files.length;
		    if (! c)
			return;
		    for (let i = 0; i < c ; i++) {
			let f = data.files[i];
			// 检查是否已经在数组中
			let files = Pal[id].send.input.files;
			let j;
			for (j = 0; j < files.length ; j++) {
			    if (files[j].path == f.path)
				break;
			}
			if (j < files.length)
			    continue; // 已在数组中, 忽略
			if (! fs.existsSync(f.path))
			    continue; // 文件不存在, 忽略
			// 检查文件是否为文件夹
			if (fs.statSync(f.path).isDirectory()) //!isFile()
			    continue; // 文件是目录, 忽略
			let g = {};
			g.name = f.name;
			g.size = f.size;
			g.path = f.path;
			g.lastModified = f.lastModified;
			g.sent = false;
			g.recv = 0;
			Pal[id].send.input.files.push(g);
		    }
		    c = Pal[id].send.input.files.length;
		    //if (! c)
		    //	return;
		    let s = '';
		    for (let i = 0; i < c ; i++) {
			let f = Pal[id].send.input.files[i];
			//if (f.sent)
			//    continue;
			s +='<tr><td width="20px" title="已接收人数">' +
				f.recv +
			    '</td><td width="100px" title="文件总字节数">'
			    + f.size + '</td><td width="120px" title="' +
			    f.name + '" style="word-break:keep-all;' +
			    'white-space:nowrap;">' + f.name +
			    '</td></tr>';
		    }
		    //if (s)
			filesToSend.innerHTML = s;
	 	    button_clear.style.display = 'inline-block';
		}

		chatWin.document.body.ondrop = function (e) {
		    let data = e.dataTransfer;
		    do_text_and_files (data, e, true);
		};
		chatWin.document.body.addEventListener('paste', (e) => {
		    let data = (e.clipboardData || window.clipboardData);
		    do_text_and_files (data, e);
		});

		chatWin.document.body.appendChild(div_in);
		div_text = chatWin.document.createElement('div');
		div_text.id = 'textinput';
		div_text.title = '这里填写要发送的文字。\n\n' +
			'在输入的文字结尾处继续输入两个回车，可快捷发送。' +
			'\n\n按 Ctrl+V 可以从别处粘贴文字到文本框。\n\n' +
			'若粘贴大量文字到文本框, 会导致软件响应迟钝！\n\n' +
			'若试图一次发送超过 5000 个字符，很可能会失败。';
		//div_text.style.position = 'relative';
		div_text.style.top = 0;
		div_text.style.bottom = 0;
		div_text.style.display = 'inline-block';
		// div_text.style.float = 'left';
		div_text.contentEditable = true;
		//div_text.style.overflow = 'scroll';
		//div_text.style.position = 'fixed';
		//div_text.style.bottom = '1px';
		//div_text.style.height = '95%';
		//div_text.style.height = 'inherit';
		//div_text.style.maxHeight = '600px';
		div_text.style.minHeight = '100px';
		div_text.style.width = 'auto';
		div_text.style.maxWidth = '400px';
		div_text.style.minWidth = '200px';
		div_text.style.caretColor = 'green';
		div_text.style.backgroundColor = 'white';
		div_text.style.color = 'black';
		div_text.style.padding = '2px';
		div_text.style.borderColor = 'green';
		div_text.style.borderWidth = '1px';
		div_text.style.borderStyle = 'solid';
		div_text.onkeyup = function(event) {
		    let tmp = div_text.innerText;
		    if (tmp.length <= 2) return;
		    if (/^\s+$/.test(tmp)) {
			div_text.innerText = '';
			return;
		    }
		    if (event.key != 'Enter')
			return;
		    label_quick_send:
		    if (tmp.slice(-3) == '\n\n\n') {
			let selection = chatWin.getSelection();
			if (! selection || ! selection.rangeCount)
			    break label_quick_send;
			let range = selection.getRangeAt(0);
			if (! range.collapsed)
			    break label_quick_send;
			if (! div_text.contains(range.startContainer))
			    break label_quick_send;
			// 试着把 range 延伸到 div_text 的尾部
			range.setEndAfter(div_text.lastChild);
			if (! range.collapsed) {
			    // 能延伸, 说明 caret 不在尾部
			    // 奇怪, 确实不在尾部, 但只差 1, 需要处理
			    if (range.startContainer != range.endContainer
				|| range.startOffset < range.endOffset - 1) {
				range.collapse(true); // 恢复原样, 折叠到开头
				break label_quick_send;
			    }
			    // 也认为到达尾部了, 但 caret 需要恢复原样
			    range.collapse(true); // 恢复原样, 折叠到开头
			}
			// 不能延伸, 说明 caret 已在尾部
			// 快捷发送只用于发送按钮，不用于邮寄按钮
			if (! Pal[id].IP || ! Pal[id].port) {
			    fail_out(
				'邮寄时咱不使用快捷发送。咱可点击邮寄按钮。');
			    return;
			}
			button_send.onclick();
			return;
		    }
		    if (Pal[id].haskey <= 0)
			return;
		    if (/^\/\/\/\/\n\s*$/.test(tmp)) { //启用、禁用、清除
			if (! Pal[id].IP || ! Pal[id].port) {
			    if (! chatWin.encryptmail) {
				chatWin.encryptmail = true; //启用邮件加密
				div_out.style.backgroundColor = 'lightblue';
				div_text.innerText = '';
				return;
			    }
			    chatWin.encryptmail = false; // 禁用邮件加密
			    div_out.style.backgroundColor = 'white';
			    // 继续运行后续代码, 清除口令。
			}
			// 清除口令
			if (chatWin.cmd_pass) {
			    chatWin.cmd_pass = '';
			    chatWin.tmp_pass = '';
			}
			chatWin.cmd_mode = false;
			div_text.style.caretColor = 'green';
			div_text.style.backgroundColor = 'white';
			div_text.style.color = 'black';
			div_text.innerText = '';
			return;
		    }
		    if (! Pal[id].IP || ! Pal[id].port) {
			if (! chatWin.encryptmail) {
			    return; //若禁用邮件加密, 任何命令都只是文本而已
			}
		    }
		    if (/^\/\/[^\s\0-/:-@[-^\x60\x7b-\x7f]*\n\s*$/.test(tmp)) {
			let word = tmp.slice(2).replace(/\s+$/, '');
			if (! word) {
			    if (! chatWin.cmd_pass) {
				chatWin.cmd_mode = false;
				div_text.style.caretColor = 'green';
				div_text.style.backgroundColor = 'white';
				div_text.style.color = 'black';
			    } else if (chatWin.cmd_mode) {
				chatWin.cmd_mode = false;
				div_text.style.caretColor = 'green';
				div_text.style.backgroundColor = 'white';
				div_text.style.color = 'black';
			    } else {
				chatWin.cmd_mode = true;
				div_text.style.caretColor = 'red';
				div_text.style.backgroundColor = 'lightblue';
				div_text.style.color = 'red';
				if (chatWin.cmd_intervalID != undefined) {
				    clearInterval (chatWin.cmd_intervalID);
				    chatWin.cmd_intervalID = undefined;
				}
				chatWin.cmd_intervalID = setTimeout (() => {
				    chatWin.cmd_mode = false;
				    div_text.style.caretColor = 'green';
				    div_text.style.backgroundColor = 'white';
				    div_text.style.color = 'black';
				}, 120000);
			    }
			    div_text.innerText = '';
			    return;
			}
			if (chatWin.cmd_pass) {
			    chatWin.cmd_mode = false;
			    div_text.style.caretColor = 'green';
			    div_text.style.backgroundColor = 'white';
			    div_text.style.color = 'black';
			    div_text.innerText = '';
			    return;
			} else {
			    if (chatWin.tmp_pass) {
				//验证未完成, 禁止修改 tmp_pass
				div_text.innerText = '';
				return;
			    }
			    // 设定 tmp_pass 的值，并发送验证
			    chatWin.tmp_pass = word;
			    div_text.innerText = '\u0002' + word + '\u0002';
			    if (Pal[id].IP && Pal[id].port)
				button_send.onclick();
			    else
				button_mail.onclick();
			    return;
			}
		    }
		    if (! /\n/.test(tmp))
			return;
		    if (! chatWin.cmd_mode)
			return;
		    if (! chatWin.cmd_pass)
			return;
		    let cmd = tmp.replace(/\n/g, '');
		    if (cmd == chatWin.old_cmd) {
			chatWin.cmd_mode = false;
			div_text.style.caretColor = 'green';
			div_text.style.backgroundColor = 'white';
			div_text.style.color = 'black';
			div_text.innerText = '';
			return;
		    }
		    div_text.innerText = '\u0002' + chatWin.cmd_pass +
		    	'\u0002' + cmd + ' ' + Date.now();
		    chatWin.old_cmd = cmd;
		    if (Pal[id].IP && Pal[id].port) {
			button_send.disabled = true;
			button_send.onclick();
			button_send.disabled = false;
		    } else {
			button_mail.disabled = true;
			button_mail.onclick();
			button_mail.disabled = false;
		    }
		    if (chatWin.cmd_intervalID != undefined) {
			clearInterval (chatWin.cmd_intervalID);
			chatWin.cmd_intervalID = undefined;
		    }
		    chatWin.cmd_intervalID = setTimeout (() => {
			chatWin.cmd_mode = false;
			div_text.style.caretColor = 'green';
			div_text.style.backgroundColor = 'white';
			div_text.style.color = 'black';
		    }, 300000);
		    return;
		};
		div_in.appendChild(div_text);
		div_file = chatWin.document.createElement('div');
		div_file.id = 'fileinput';
		//div_file.style.position = 'fixed';
		//div_file.style.bottom = 0;
		//div_file.style.display = 'inline-block';
		div_file.style.float = 'right';
		div_in.appendChild(div_file);
		//textin = chatWin.document.createElement('textarea');
		//textin.id = 'textin';
		//textin.rows = 4;
		//textin.cols = 60;
		//textin.maxlength = 2000;
		//textin.minlength = 1;
		//textin.oninput = function() {
		//	let tmp = textin.value;
		//	if (tmp.length <= 2) return;
		//	if (tmp.replace(/[\s]/g, '').length == 0) {
		//	    textin.value = '';
		//	    return;
		//	}
		//	if (tmp.slice(-3) != '\n\n\n' &&
		//	    tmp.slice(-6) != '\r\n\r\n\r\n')
		//		return;
		//	//textin.value = tmp.replace (/(^\s*)|(\s*$)/g,'');
		//	button_send.onclick();
		//};
		//textin.placeholder = '这里填写要发送的文字。提示: ' +
		//	'在输入的文字结尾处继续输入三个回车，即可快捷发送。';
		//div_text.appendChild(textin);

		filesToSend = chatWin.document.createElement('table');
		filesToSend.id = 'filesToSend';
		//filesToSend.display = 'inline-block';
		//filesToSend.style.float = 'left';
		filesToSend.style.tableLayout = 'fixed';
		filesToSend.style.width = '100px';
		filesToSend.style.maxWidth = '300px';
		//let filesToSend_tr = chatWin.document.createElement('tr');
		//let filesToSend_td = chatWin.document.createElement('td');

		checkbox_cc = chatWin.document.createElement('input');
		checkbox_cc.id = 'checkbox_cc';
		checkbox_cc.type = 'checkbox';
		checkbox_cc.title = '勾选可发送给多人。\n' +
			'请在 miao 的设置中指定接收者。\n' +
			'点击“发送”按钮之前，您可以先刷新好友。';
 		//checkbox_cc.style.display = 'inline-block';

		checkbox_cc.onclick = function () {
			//alert('点击了');
		};

		div_file.insertAdjacentHTML ('beforeEnd', ' ');

		div_file.appendChild(checkbox_cc);

		div_file.insertAdjacentHTML ('beforeEnd', '群发&nbsp;');

		button_send = chatWin.document.createElement('button');
		button_send.type = 'button';
		button_send.id = 'button_send';
		button_send.title = '开始发消息和文件。\n\n' +
			'自动跳过上一批刚发送过的文件, 不重复发送它们；\n\n' +
			'要想再次发送它们, 须先清空列表，再添加附件。';

		button_send.onclick = button_send_onclick;

 		//button_send.style.display = 'inline-block';
		if (Pal[id].offline) { // 对方不在线
 			//button_send.style.display = 'none'; // 隐藏发送按钮
			button_send.innerText = '发送（对方可能已离线）';
		} else {
			button_send.innerText = '发送';
		}
		div_file.appendChild (button_send);
		div_file.insertAdjacentHTML ('beforeEnd', '&nbsp;');

		button_mail = chatWin.document.createElement('button');
		button_mail.type = 'button';
		button_mail.id = 'button_mail';

		button_mail.onclick = button_mail_onclick;
 		button_mail.style.display = 'none'; // 先不显示按钮

		if (effective_mailuser && effective_mailpass &&
			//Pal[id].version.split('#')[2] &&
			Pal[id].version.split('#')[8]) {
		    let account = Pal[id].version.split('#')[8];
		    if (! /@/.test(account))
			account += '@ethereal.email';
 		    // 查询是否已经为好友
		    let tmp = stored_friends.split(/\s[^\r\n]*[\r\n]{0,}/);
		    if (tmp.indexOf(account) == -1) {
			    button_mail.innerText = '加为好友'; // 不是好友
			    button_mail.title = '加为好友后才能邮寄。';
		    } else {
			    button_mail.innerText = '邮寄';   // 已是好友
			    button_mail.title = '开始邮寄消息和文件。\n\n' +
				'自动跳过已寄出文件, 不重复邮寄。\n\n消息和' +
				'文件全部成功寄出后, 才会在聊天窗显示发给对' +
				'方的内容; 附件越大, 需要等待的时间越长。';
		    }
		    button_mail.style.display = 'inline'; // 显示按钮
		}
		div_file.appendChild (button_mail);

		div_file.insertAdjacentHTML ('beforeEnd', '&nbsp;');

		let input_label = chatWin.document.createElement('label');
		input_label.innerHTML = '添加附件...';
		input_label.tabIndex = 0;
		input_label.title =
			'若拖拽的文件中含有文件夹, 这些文件夹会被忽略！\n\n' +
			'拒绝添加上次刚发送过的文件，除非先清空列表。\n\n' +
			'需先点击此按钮才能显示【清空列表】按钮。';
		input_label.onkeyup = function (event) {
		    if (event.key == 'Enter') {
			try {
			    input.click();
			} catch (ex) {
			    fail ('无法打开选择文件对话框: ' + ex);
			}
		    }
		};

		input = chatWin.document.createElement('input');
		input.type = 'file';
		//input.style.opacity = 0;
		input.style.display = 'none';
		input.multiple = true;
		input.title = input_label.title;
		input.onclick = function () {
	 		button_clear.style.display = 'inline-block';
		};

		input.onchange = function (event) {
			let c = this.files.length;
			if (! c)
			    return;
			// 保存 files 信息到 Pal[id].send.input.files 数组中
			for (let i = 0; i < c ; i++) {
			    let f = this.files[i];
			    // 检查是否已经在数组中
			    let files = Pal[id].send.input.files;
			    let j;
			    for (j = 0; j < files.length ; j++) {
				if (files[j].path == f.path)
				    break;
			    }
			    if (j < files.length)
				continue; // 已在数组中, 忽略
			    if (! fs.existsSync(f.path))
				continue; // 文件不存在, 忽略
			    // 检查文件是否为文件夹
			    if (fs.statSync(f.path).isDirectory()) //!isFile()
				continue; // 文件是目录, 忽略
			    let g = {};
				g.name = f.name;
				g.size = f.size;
				g.path = f.path;
				g.lastModified = f.lastModified;
				g.sent = false;
				g.recv = 0;
			    Pal[id].send.input.files.push(g);
			}
			c = Pal[id].send.input.files.length;
			//if (! c)
			//    return;
			let s = '';
			for (let i = 0; i < c ; i++) {
			    let f = Pal[id].send.input.files[i];
			    //if (f.sent)
				//continue;
			    s +='<tr><td width="20px" title="已接收人数">' +
				f.recv +
				'</td><td width="100px" title="文件总字节数">'
				+ f.size + '</td><td width="120px" title="' +
				f.name + '" style="word-break:keep-all;' +
				'white-space:nowrap;">' + f.name +
				'</td></tr>';
			}
			//if (s)
			    filesToSend.innerHTML = s;
	 		button_clear.style.display = 'inline-block';
		};

		div_file.appendChild(input_label);

		if (! Pal[id].send)
		    Pal[id].send = {};
		Pal[id].send.input = {};

		if (! Pal[id].send.input.files)
		    Pal[id].send.input.files = [];

		// 在 chrome 中 <input type=file> 必须是 <label> 的 child 才行,
		// 否则 <label> 不起作用
		input_label.appendChild(input);

		div_file.insertAdjacentHTML ('beforeEnd', '&nbsp;');

		button_clear = chatWin.document.createElement('button');
		button_clear.type = 'button';
		button_clear.id = 'button_clear';
		button_clear.innerText = '清空列表';
		button_clear.title =
			'若列表有错, 可清空列表, 再重新添加附件。\n\n' +
			'上次刚发送过的文件也可能在列表中, ' +
			'需要清空列表后才允许再次添加它们。\n\n' +
			'清空列表后, 先前那些正在发送的文件将无法完成发送。';

		button_clear.onclick = function () {
		    if (chatWin.sending > 0) {
			fail_out('有 ' + chatWin.sending +
			    ' 个文件正在发送中, 请等待发送完成后再清空。');
			return;
		    }
		    chatWin.sending = 0;
		    chatWin.file_send_SN = undefined;
		    Pal[id].send.input.files = [];
		    filesToSend.innerHTML = '';
 		    button_clear.style.display = 'none';
		    SendFileStateChanged = false;
		    input.value = null;
		};

 		button_clear.style.display = 'none'; //'inline-block';
		div_file.appendChild (button_clear);

 		//filesToSend.style.display = 'inline';
		div_file.appendChild(filesToSend);
		p = chatWin.document.createElement('p');
		//p.style.color = 'green';
		p.style.margin = 0;
		//p.style.padding = 0;
		p.style.textAlign = 'center';
		div_out.appendChild(p);

		let tmp;
		tmp = chatWin.document.createElement('input');
		tmp.type = 'button';
		tmp.value = '打开 ' + Pal[id].nick + ' 的共享';
		tmp.title = '用文件管理器打开 '+Pal[id].nick+' 的 SMB 共享' +
			    '。\n这将打开对方操作系统中的 SMB 共享资源。\n' +
			    '对方可以不是 miao 用户。\n' +
			    '如果对方没有提供 SMB 共享资源，则无法访问。\n' +
			    '如果对方操作系统中提供了 SMB 共享资源，' +
			    '则无论对方是否启动即时通讯软件，\n咱都' +
			    '可以访问（只要咱能够获取对方的 IP 地址）。';
		tmp.onclick = function () {
		    openExternal('smb://' + (/:/.test(Pal[id].IP) ?
			('[' + Pal[id].IP + ']') : Pal[id].IP) + '/');
		};
		p.appendChild(tmp);

		tmp = chatWin.document.createElement('input');
		tmp.type = 'button';
		tmp.value = '打开 ' + Pal[id].nick + ' 的主页';
		tmp.title = '用系统默认浏览器打开 '+Pal[id].nick+' 的网页' +
			    ', 这是要访问对方电脑上的网页文件服务器。\n\n' +
			    'miao 软件内建的网页服务器采用 http 协议, ' +
			    '端口号是 5080。\n\n对方如果不是 miao 用户, ' +
			    '则需要在他的电脑上安装一个 http 服务器,\n' +
			    '并采用端口 5080, 否则, 咱点击' +
			    '此按钮时无法访问他的网站。\n\n' +
			    '提示: 本机网页服务的文件夹是 ' +
				path.resolve('..', 'wwwroot') +
			    '\n咱可以把想要共享出去的文件放在里面，让别人' +
			    '下载。【重要】同步文件夹也放在这里, 因而可以' +
			    '被任何人访问! 网页共享文件夹以及同步文件夹' +
			    '的设计理念是“方便共享”而不是“安全保密”。';
		tmp.onclick = function () {
		    openExternal('http://' + (/:/.test(Pal[id].IP) ?
			('[' + Pal[id].IP + ']') : Pal[id].IP) +
			':5080/');
		};
		p.appendChild(tmp);

		let palDirName = Pal[id].nick +
		    id.replace(/@.*$/,'').replace(/:/g,'_');

		let sync = path.resolve('..', 'wwwroot', palDirName, 'sync');

		tmp = chatWin.document.createElement('input');
		tmp.type = 'button';
		tmp.value = '打开 ' + Pal[id].nick + ' 的同步';
		tmp.title = '用文件管理器打开本机为 ' + Pal[id].nick +
			    ' 保留的同步文件夹:\n\n' + sync + '\n\n' +
			    '咱把文件或文件夹放在同步文件夹里面，对方的 ' +
			    'miao 会自动获取这些文件或文件夹。\n如果对方' +
			    '已有同名文件, 则只会获取较新的文件。\n\n' +
			    '同步文件夹是向全网公开的，任何人都能通过本机' +
			    '提供的网页服务器而访问本机的所有同步文件夹的' +
			    '内容。\n机密文件应该在加密之后再进行同步, ' +
			    '不要直接以明文的形式放在同步文件夹中。';
		tmp.onclick = function () {
		    if (! mkdir_recursive(sync)) {
			fail_out(' 创建文件夹 ' + sync +
				' 失败, 无法打开同步文件夹!');
			return;
		    }
		    setTimeout(openItem, 1, sync);
		};
		p.appendChild(tmp);

		//tmp = chatWin.document.createElement('input');
		//tmp.type = 'button';
		//tmp.value = '打开 ' + Pal[id].nick + ' 的网盘';
		//tmp.title = '用文件管理器打开 '+Pal[id].nick+' 的 WebDAV ' +
		//	    '网盘。\n\nmiao 软件内建的 WebDAV 文件' +
		//	    '共享服务器采用 http 协议，服务端口号是 5050, ' +
		//	    '网址路径是 /webdav/\n\n对方如果不是 miao 用户,' +
		//	    ' 则需要在他的电脑上安装一个 WebDAV 服务器, \n' +
		//	    '否则, 咱点击此按钮时无法访问他的共享文件夹。' +
		//	    '\n\n提示: 本机 WebDAV 服务的文件夹是 ' +
		//		path.resolve('..', 'webdav') +
		//	    '\n咱可以把想要共享出去的文件放在里面，让别人' +
		//	    '下载。';
		//tmp.onclick = function () {
		//    openExternal('dav://' + (/:/.test(Pal[id].IP) ?
		//	('[' + Pal[id].IP + ']') : Pal[id].IP) +
		//	':5050/webdav/');
		//};
		//p.appendChild(tmp);

		let services = Pal[id].services;
		if (services) {
		    services = services.split(/\s*[\r\n]\s{0,}/);
		    for (let i = 0; i < services.length; i++) {
			let s = services[i].replace(/^\s+/, '').split(/\s+/);
			if (! s)
			    continue;
			let port = (s[1] ? ':' + s[1] : '');
			let path = ('/' + (s[2] ? s[2] : ''))
					.replace(/\/{2,}/g, '/');
			let Url = s[0] + '://' + 
					(/:/.test(Pal[id].IP) ?
					('[' + Pal[id].IP + ']') : Pal[id].IP)
					+ port + path;
			tmp = chatWin.document.createElement('input');
			tmp.type = 'button';
			tmp.value = '打开 ' + Pal[id].nick + ' 的 ' + s[0];
			tmp.title = '用系统默认应用打开 ' + Pal[id].nick +
				    ' 的服务 ' + Url;
			tmp.onclick = function () {
				openExternal(Url);
			};
			p.appendChild(tmp);
		    }
		}

		let f = path.resolve('..', 'shared', palDirName,
			'history.html');
		if (! fs.existsSync(f)) {
		    return;
		}
		if (! fs.statSync(f).isFile()) { //isDirectory()
		    return;
		}
		tmp = chatWin.document.createElement('input');
		tmp.type = 'button';
		tmp.value = '查看历史记录';
		tmp.title = '用系统默认浏览器查看双方的聊天记录';
		tmp.onclick = function () {
		    setTimeout(openItem, 1, f);
		};
		p.appendChild(tmp);
	    } // create_in_out_boxes () 局部函数定义结束

	    if (chatWin.document.getElementById('outputbox')) {
	    	if (! chatWin.document.getElementById('inputbox')) {
		    alert ('Fatal! output box exists, but no inputbox.');
		    return;
		}
		div_out = chatWin.document.getElementById('outputbox');
		div_in = chatWin.document.getElementById('inputbox');
		//textin = chatWin.document.getElementById('textin');
		div_text = chatWin.document.getElementById('textinput');
		button_send = chatWin.document.getElementById('button_send');
		button_mail = chatWin.document.getElementById('button_mail');
		button_clear = chatWin.document.getElementById('button_clear');
		checkbox_cc = chatWin.document.getElementById('checkbox_cc');
		filesToSend = chatWin.document.getElementById('filesToSend');
	    } else {
	    	if (chatWin.document.getElementById('inputbox')) {
		    alert ('Fatal! No output box, but inputbox exists.');
		    return;
		}
		create_in_out_boxes ();
	    }

//	    if (gain_focus)
//		chatWin.focus ();

	    if (typeof (msg) !== 'object') // 是双击跳出聊天窗, 不是收到消息
		return;

	    // 收到消息

    	    let textstr = msg.mainbody;
	    p = chatWin.document.createElement('p');
 	    //p.style.color = 'green';
	    p.style.margin = 0;
	    //p.style.padding = 0;
	    div_out.appendChild(p);

	    function receive_all () {
		for (let i = 0; i < p.lastChild.children.length; i++) {
		    let nd = p.lastChild.children[i];
		    if (nd.nodeType != 1)
			continue;
		    if (nd.tagName != 'INPUT')
			continue;
		    if (nd.nodeName != 'INPUT')
			continue;
		    if (nd.type != 'button')
			continue;
		    if (nd.value == '全部接收')
			nd.disabled = true;  // 禁止再次点击【全部接收】按钮
		    if (nd.value != '接收')
			continue;
		    if (nd.style.display == 'none')
			continue;
		    if (typeof nd.onclick !== 'function')
			continue;
		    nd.onclick();
		}
	    }

	    function check_attached_files (prompt) {
		let t = textstr.split('\0');
		if (t[0].slice(0, 6) == '\u0001gzip\u0001') {
			const { gunzipSync } = require ('zlib');
			textstr = gunzipSync(
				Buffer.from(textstr.slice(6), 'base64'))
				.toString();
			t = textstr.split('\0');
		}
		//if (Pal[id].haskey > 0 && prompt == '#' &&
		//	/stdout:<pre><samp>.*</samp></pre>stderr:/.test(t[0]))
		//{
		//	//来自 exec 的输出
		//}
		if (Pal[id].haskey > 0 && prompt == '#' &&
			(! Pal[id].IP || ! Pal[id].port)) {
			if (! chatWin.encryptmail) {
			    chatWin.encryptmail = true;
			    div_out.style.backgroundColor = 'lightblue';
			}
		}

		// 执行命令的条件：
		// 1、对方必须是好友
		// 2、对方发来的消息必须是密文
		// 3、咱设置的静态口令不是空的
		// 4、对方发来的消息不可以是多行, 即, 不可以含有换行符
		// 5、对方发来的消息含有正确的口令
		label_runcmd:
		if (Pal[id].haskey > 0 && prompt == '#' &&
			stored_rexecStaticPass) {
		    if (/^\s*Passed: 操作系统版本: /.test(t[0])) {
			if (chatWin.tmp_pass)
			    chatWin.cmd_pass = chatWin.tmp_pass;
			break label_runcmd;
		    }
		    if (/^\s*Error: /.test(t[0])) {
			if (chatWin.tmp_pass)
			    chatWin.tmp_pass = '';
			if (chatWin.cmd_mode) {
			    chatWin.cmd_mode = false;
			    div_text.style.caretColor = 'green';
			    div_text.style.backgroundColor = 'white';
			    div_text.style.color = 'black';
			}
			break label_runcmd;
		    }
		    if (/\n/.test(t[0]))
			break label_runcmd;
		    let len = stored_rexecStaticPass.length + 2;
		    if (t[0].slice(0, 1) != '\u0002')
			break label_runcmd;
		    if (chatWin.pass_errors || t[0].slice(1, len)
				!= stored_rexecStaticPass + '\u0002') {
			if (chatWin.pass_errors < 16)
			    chatWin.pass_errors ++;
			//let text = div_text.innerText; // 备份
			div_text.innerText = ' Error: 口令错误, 序号: ' +
				chatWin.pass_errors + '<br>';
			if (chatWin.pass_intervalID != undefined) {
			    clearInterval (chatWin.pass_intervalID);
			    chatWin.pass_intervalID = undefined;
			}
			chatWin.pass_intervalID = setTimeout (() => {
			    chatWin.pass_errors = 0;
			}, (30000 << chatWin.pass_errors));
			if (Pal[id].IP && Pal[id].port) {
			    button_send.disabled = true;
			    button_send.onclick();
			    button_send.disabled = false;
			} else {
			    button_mail.disabled = true;
			    button_mail.onclick();
			    button_mail.disabled = false;
			}
			//div_text.innerText = text; // 恢复
			break label_runcmd;
		    }
		    const {exec, execFileSync} = require('child_process');
		    let cmdline = t[0].slice(len);//.replace(/^\s+/, '');
		    // 将 &nbsp; 转为空格
		    cmdline = cmdline.replace(/\s/g, ' ');

		    if (Pal[id].IP && Pal[id].port) {
			checkbox_cc.checked = false; //禁止群发
			button_send.disabled = true;
		    }
		    if (! cmdline) {
			//let text = div_text.innerText; // 备份
			div_text.innerText = ' Passed: 操作系统版本: ' +
				os.type() + ' ' + os.arch() + ' ' +
				os.release() + '<br>';
			if (Pal[id].IP && Pal[id].port) {
			    button_send.disabled = true;
			    button_send.onclick();
			    button_send.disabled = false;
			} else {
			    button_mail.disabled = true;
			    button_mail.onclick();
			    button_mail.disabled = false;
			}
			//div_text.innerText = text; // 恢复
			break label_runcmd;
		    }
		    let cmd_time = Number(cmdline.replace(/^.* /, ''));
		    if (! Number.isInteger(cmd_time))
			break label_runcmd;
		    if (! miaoconfig.lastExecTime)
			miaoconfig.lastExecTime = {};

		    if (miaoconfig.lastExecTime[id.slice(0,16)])
			if (miaoconfig.lastExecTime[id.slice(0,16)]
				>= cmd_time)
			    break label_runcmd;
		    miaoconfig.lastExecTime[id.slice(0,16)] = cmd_time;
		    miaoconfig_needupdate = true;
		    update_miaoconfig();

		    cmdline = cmdline.replace(/ [0-9]+$/, '');

		    if (/^pwd\s*$/.test(cmdline)) {
			//let text = div_text.innerText; // 备份
			div_text.innerText = ' 当前工作目录是: ' +
				rexec_cwd.replace(/ /g, '&nbsp;') + '<br>';
			if (Pal[id].IP && Pal[id].port) {
			    button_send.disabled = true;
			    button_send.onclick();
			    button_send.disabled = false;
			} else {
			    button_mail.disabled = true;
			    button_mail.onclick();
			    button_mail.disabled = false;
			}
			//div_text.innerText = text; // 恢复
			break label_runcmd;
		    }
		    if (/^cd(\s+\S.*){0,1}$/.test(cmdline)) {
			cmdline = cmdline.replace(/^cd\s{0,}/, '');
			if (! cmdline) {
			    cmdline = process.cwd();
			//} else if (/^~/.test(cmdline)) {
			} else if (cmdline.slice(0,1) == '~' && (
				   cmdline.slice(1,2) == '' ||
				   cmdline.slice(1,2) == path.sep ||
				   cmdline.slice(1,2) == '/' )) {
			    cmdline = path.resolve(HOME,
				cmdline.replace(/^~/,'.'));
			}
			let f_path = path.resolve(rexec_cwd, cmdline);
			if (! fs.existsSync(f_path)) {
			    //let text = div_text.innerText; // 备份
			    div_text.innerText = ' Error: 路径不存在: ' +
				f_path.replace(/ /g, '&nbsp;') + '<br>';
			    if (Pal[id].IP && Pal[id].port) {
				button_send.disabled = true;
				button_send.onclick();
				button_send.disabled = false;
			    } else {
				button_mail.disabled = true;
				button_mail.onclick();
				button_mail.disabled = false;
			    }
			    //div_text.innerText = text; // 恢复
			    break label_runcmd;
			}
			let stat = fs.statSync(f_path);
			if (! stat.isDirectory()) {
			    //let text = div_text.innerText; // 备份
			    div_text.innerText = ' Error: 路径不是目录: ' +
				f_path.replace(/ /g, '&nbsp;') + '<br>';
			    if (Pal[id].IP && Pal[id].port) {
				button_send.disabled = true;
				button_send.onclick();
				button_send.disabled = false;
			    } else {
				button_mail.disabled = true;
				button_mail.onclick();
				button_mail.disabled = false;
			    }
			    //div_text.innerText = text; // 恢复
			    break label_runcmd;
			}
			rexec_cwd = f_path;
			//let text = div_text.innerText; // 备份
			div_text.innerText = ' 成功将当前工作目录设定为: ' +
				f_path.replace(/ /g, '&nbsp;') + '<br>';
			if (Pal[id].IP && Pal[id].port) {
			    button_send.disabled = true;
			    button_send.onclick();
			    button_send.disabled = false;
			} else {
			    button_mail.disabled = true;
			    button_mail.onclick();
			    button_mail.disabled = false;
			}
			//div_text.innerText = text; // 恢复
			break label_runcmd;
		    }
		    if (/^send\s*$/.test(cmdline)) {
			//let text = div_text.innerText; // 备份
			if (Pal[id].IP && Pal[id].port) {
			    div_text.innerText = ' 成功点击发送按钮。<br>';
			    button_send.disabled = false;
			    button_send.onclick();
			} else {
			    div_text.innerText = ' Error: 不该使用此命令<br>';
			    button_mail.disabled = true;
			    button_mail.onclick();
			    button_mail.disabled = false;
			}
			//div_text.innerText = text; // 恢复
			break label_runcmd;
		    }
		    if (/^mail\s*$/.test(cmdline)) {
			//let text = div_text.innerText; // 备份
			if (Pal[id].IP && Pal[id].port) {
			    div_text.innerText = ' Error: 不该使用此命令<br>';
			    button_send.disabled = true;
			    button_send.onclick();
			    button_send.disabled = false;
			} else {
			    div_text.innerText = ' 成功点击邮寄按钮。<br>' +
				'提示: 点击了邮寄按钮, 不等于已经开始邮寄, ' +
				'因为有出错的情况, 比如, 若本次点击邮寄按钮' +
				'距离上次点击邮寄按钮的间隔时间太短, 就可能' +
				'会失败。<br>';
			    button_mail.disabled = false;
			    button_mail.onclick();
			}
			//div_text.innerText = text; // 恢复
			break label_runcmd;
		    }
		    if (/^clear\s*$/.test(cmdline)) {
			button_clear.onclick();
			//let text = div_text.innerText; // 备份
			div_text.innerText = ' 成功执行清空列表命令。<br>';
			if (Pal[id].IP && Pal[id].port) {
			    button_send.disabled = true;
			    button_send.onclick();
			    button_send.disabled = false;
			} else {
			    button_mail.disabled = true;
			    button_mail.onclick();
			    button_mail.disabled = false;
			}
			//div_text.innerText = text; // 恢复
			break label_runcmd;
		    }
		    if (/^show\s*$/.test(cmdline)) {
			let c = Pal[id].send.input.files.length;
			let s = '';
			for (let i = 0; i < c ; i++) {
			    let f = Pal[id].send.input.files[i];
			    s += (f.sent ? '已发' : '待发') + ' 修改时间 ' +
				 (new Date(f.lastModified)).toLocaleString
				 (date_locale, date_options) + ' 文件长度 ' +
				 f.size + ' 字节 ' +
				 f.path.replace(/ /g, '&nbsp;') + '<br>';
			}
			//let text = div_text.innerText; // 备份
			if (s)
			    div_text.innerText = '<br/>' + s;
			else
			    div_text.innerText = ' 附件列表为空。<br/>';
			if (Pal[id].IP && Pal[id].port) {
			    button_send.disabled = true;
			    button_send.onclick();
			    button_send.disabled = false;
			} else {
			    button_mail.disabled = true;
			    button_mail.onclick();
			    button_mail.disabled = false;
			}
			//div_text.innerText = text; // 恢复
			break label_runcmd;
		    }
		    if (/^add\s+\S/.test(cmdline)) {
			let f_path = path.resolve(rexec_cwd, cmdline
				.replace(/^add\s+/, ''));
			let files = Pal[id].send.input.files;
			let j;
			for (j = 0; j < files.length ; j++) {
			    if (files[j].path == f_path)
				break;
			}
			if (j < files.length) {
			    //let text = div_text.innerText; // 备份
			    div_text.innerText = ' Error: 文件 ' +
				f_path.replace(/ /g, '&nbsp;') +
				' 曾经添加过, 拒绝再次添加。' +
				'提示: 清空列表后可再次添加。<br>';
			    if (Pal[id].IP && Pal[id].port) {
				button_send.disabled = true;
				button_send.onclick();
				button_send.disabled = false;
			    } else {
				button_mail.disabled = true;
				button_mail.onclick();
				button_mail.disabled = false;
			    }
			    //div_text.innerText = text; // 恢复
			    break label_runcmd;
			}
			if (! fs.existsSync(f_path)) {
			    //let text = div_text.innerText; // 备份
			    div_text.innerText =
				' Error: 路径不存在, 无法添加: ' +
				f_path.replace(/ /g, '&nbsp;') + '<br>';
			    if (Pal[id].IP && Pal[id].port) {
				button_send.disabled = true;
				button_send.onclick();
				button_send.disabled = false;
			    } else {
				button_mail.disabled = true;
				button_mail.onclick();
				button_mail.disabled = false;
			    }
			    //div_text.innerText = text; // 恢复
			    break label_runcmd;
			}
			let stat = fs.statSync(f_path);
			if (! stat.isFile()) { //isDirectory()
			    //let text = div_text.innerText; // 备份
			    div_text.innerText =
				' Error: 不是普通文件, 无法添加: ' +
				f_path.replace(/ /g, '&nbsp;') + '<br>';
			    if (Pal[id].IP && Pal[id].port) {
				button_send.disabled = true;
				button_send.onclick();
				button_send.disabled = false;
			    } else {
				button_mail.disabled = true;
				button_mail.onclick();
				button_mail.disabled = false;
			    }
			    //div_text.innerText = text; // 恢复
			    break label_runcmd;
			}
			let g = {};
			g.name = path.basename(f_path);
			g.size = stat.size;
			g.path = f_path;
			g.lastModified = stat.mtime.getTime();
			g.sent = false;
			g.recv = 0;
			Pal[id].send.input.files.push(g);
			let c = Pal[id].send.input.files.length;
			let s = '';
			for (let i = 0; i < c ; i++) {
			    let f = Pal[id].send.input.files[i];
			    //if (f.sent)
				//continue;
			    s +='<tr><td width="20px" title="已接收人数">' +
				f.recv +
				'</td><td width="100px" title="文件总字节数">'
				+ f.size + '</td><td width="120px" title="' +
				f.name + '" style="word-break:keep-all;' +
				'white-space:nowrap;">' + f.name +
				'</td></tr>';
			}
			//if (s)
			    filesToSend.innerHTML = s;
			button_clear.style.display = 'inline-block';

			//let text = div_text.innerText; // 备份
			div_text.innerText = ' 已添加附件 (' + g.size +
				'字节): ' +
				g.path.replace(/ /g, '&nbsp;') + '<br>';
			if (Pal[id].IP && Pal[id].port) {
			    button_send.disabled = true;
			    button_send.onclick();
			    button_send.disabled = false;
			} else {
			    div_text.innerText += '注意不要邮寄超大附件<br>';
			    button_mail.disabled = true;
			    button_mail.onclick();
			    button_mail.disabled = false;
			}
			//div_text.innerText = text; // 恢复
			break label_runcmd;
		    }
		    if (/\S/.test(cmdline))
		    {
			if (! OEMCP && process.platform == 'win32') {
			    let str;
			    try {
				str = execFileSync('chcp').toString();
			    } catch (e) {
				str = '';
			    }
			    if (! str) {
				//let text = div_text.innerText; // 备份
				div_text.innerText = ' 请注意: 我的 Windows '
					+ '系统不完整, 缺少 chcp 命令, '
					+ '命令的执行结果可能会有乱码！<br>';
				if (Pal[id].IP && Pal[id].port) {
				    button_send.disabled = true;
				    button_send.onclick();
				    button_send.disabled = false;
				} else {
				    button_mail.disabled = true;
				    button_mail.onclick();
				    button_mail.disabled = false;
				}
				//button_send.disabled = false;
				//div_text.innerText = text; // 恢复
				//return;
			    }
			    OEMCP = str.replace(/^[^\d]+(\d+)[\r\n]*$/, '$1');
			    if (! OEM_charset) {
				let cp2charset = {
					37 :	'IBM037',
					437 :	'IBM437',
					500 :	'IBM500',
					708 :	'ASMO-708',
					720 :	'DOS-720',
					737 :	'ibm737',
					775 :	'ibm775',
					850 :	'ibm850',
					852 :	'ibm852',
					855 :	'IBM855',
					857 :	'ibm857',
					858 :	'IBM00858',
					860 :	'IBM860',
					861 :	'ibm861',
					862 :	'DOS-862',
					863 :	'IBM863',
					864 :	'IBM864',
					865 :	'IBM865',
					866 :	'cp866',
					869 :	'ibm869',
					870 :	'IBM870',
					874 :	'windows-874',
					875 :	'cp875',
					932 :	'shift_jis',
					936 :	'gb2312',
					949 :	'ks_c_5601-1987',
					950 :	'big5',
					1026 :	'IBM1026',
					1047 :	'IBM01047',
					1140 :	'IBM01140',
					1141 :	'IBM01141',
					1142 :	'IBM01142',
					1143 :	'IBM01143',
					1144 :	'IBM01144',
					1145 :	'IBM01145',
					1146 :	'IBM01146',
					1147 :	'IBM01147',
					1148 :	'IBM01148',
					1149 :	'IBM01149',
					1200 :	'utf-16',
					1201 :	'UnicodeFFFE',
					1250 :	'windows-1250',
					1251 :	'windows-1251',
					1252 :	'Windows-1252',
					1253 :	'windows-1253',
					1254 :	'windows-1254',
					1255 :	'windows-1255',
					1256 :	'windows-1256',
					1257 :	'windows-1257',
					1258 :	'windows-1258',
					1361 :	'Johab',
					10000 :	'macintosh',
					10001 :	'x-mac-japanese',
					10002 :	'x-mac-chinesetrad',
					10003 :	'x-mac-korean',
					10004 :	'x-mac-arabic',
					10005 :	'x-mac-hebrew',
					10006 :	'x-mac-greek',
					10007 :	'x-mac-cyrillic',
					10008 :	'x-mac-chinesesimp',
					10010 :	'x-mac-romanian',
					10017 :	'x-mac-ukrainian',
					10021 :	'x-mac-thai',
					10029 :	'x-mac-ce',
					10079 :	'x-mac-icelandic',
					10081 :	'x-mac-turkish',
					10082 :	'x-mac-croatian',
					20000 :	'x-Chinese-CNS',
					20001 :	'x-cp20001',
					20002 :	'x-Chinese-Eten',
					20003 :	'x-cp20003',
					20004 :	'x-cp20004',
					20005 :	'x-cp20005',
					20105 :	'x-IA5',
					20106 :	'x-IA5-German',
					20107 :	'x-IA5-Swedish',
					20108 :	'x-IA5-Norwegian',
					20127 :	'us-ascii',
					20261 :	'x-cp20261',
					20269 :	'x-cp20269',
					20273 :	'IBM273',
					20277 :	'IBM277',
					20278 :	'IBM278',
					20280 :	'IBM280',
					20284 :	'IBM284',
					20285 :	'IBM285',
					20290 :	'IBM290',
					20297 :	'IBM297',
					20420 :	'IBM420',
					20423 :	'IBM423',
					20424 :	'IBM424',
					20833 :	'x-EBCDIC-KoreanExtended',
					20838 :	'IBM-Thai',
					20866 :	'koi8-r',
					20871 :	'IBM871',
					20880 :	'IBM880',
					20905 :	'IBM905',
					20924 :	'IBM00924',
					20932 :	'EUC-JP',
					20936 :	'x-cp20936',
					20949 :	'x-cp20949',
					21025 :	'cp1025',
					21866 :	'koi8-u',
					28591 :	'iso-8859-1',
					28592 :	'iso-8859-2',
					28593 :	'iso-8859-3',
					28594 :	'iso-8859-4',
					28595 :	'iso-8859-5',
					28596 :	'iso-8859-6',
					28597 :	'iso-8859-7',
					28598 :	'iso-8859-8',
					28599 :	'iso-8859-9',
					28603 :	'iso-8859-13',
					28605 :	'iso-8859-15',
					29001 :	'x-Europa',
					38598 :	'iso-8859-8-i',
					50220 :	'iso-2022-jp',
					50221 :	'csISO2022JP',
					50222 :	'iso-2022-jp',
					50225 :	'iso-2022-kr',
					50227 :	'x-cp50227',
					51932 :	'euc-jp',
					51936 :	'EUC-CN',
					51949 :	'euc-kr',
					52936 :	'hz-gb-2312',
					54936 :	'GB18030',
					57002 :	'x-iscii-de',
					57003 :	'x-iscii-be',
					57004 :	'x-iscii-ta',
					57005 :	'x-iscii-te',
					57006 :	'x-iscii-as',
					57007 :	'x-iscii-or',
					57008 :	'x-iscii-ka',
					57009 :	'x-iscii-ma',
					57010 :	'x-iscii-gu',
					57011 :	'x-iscii-pa',
					65000 :	'utf-7',
					65001 :	'utf-8',
					65005 :	'utf-32',
					65006 :	'utf-32BE',
				};
				if (cp2charset[OEMCP])
				    OEM_charset = cp2charset[OEMCP];
			    }
			}
			exec(cmdline, { cwd: rexec_cwd,
					encoding: 'buffer' },
					(error, stdout, stderr) => {
				let str = '';
				if (error) {
				    // 先用 utf8 解码
				    str = error.toString();
				    // 若 str 含有 U+FFFD, 则表示解码失败
				    if (/\ufffd/.test(str)) { //&& OEM_charset
					//// 于是再用系统默认编码来解码
					//str = iconv.decode(
					//  Buffer.from(error), // 此用法错误
					//  OEM_charset);
					str = JSON.stringify(error);
				    }
				    str = ' Error: ' + str
					//error.toString().slice(0, 2000)
					.replace(/\r*\n/g, '<br>') + '<br>';
				}
				// 先用 utf8 解码
				let str1 = stdout.toString();
				// 若 str1 含有 U+FFFD, 则表示解码失败
				if (/\ufffd/.test(str1) && OEM_charset) {
				    // 于是再用系统默认编码来解码
				    str1 = iconv.decode(stdout, OEM_charset);
				}
				// 先用 utf8 解码
				let str2 = stderr.toString();
				// 若 str2 含有 U+FFFD, 则表示解码失败
				if (/\ufffd/.test(str2) && OEM_charset) {
				    // 于是再用系统默认编码来解码
				    str2 = iconv.decode(stderr, OEM_charset);
				}
				str += 'stdout:<pre><samp>' + str1
						.replace(/&/, '&amp;')
						.replace(/ /g, '&nbsp;')
						.replace(/</g, '&lt;') +
					'</samp></pre>stderr: ' + str2
						.replace(/\r*\n/g, '<br>') +
					'<br>';
				if (str.length > 1000) {
				    const { gzipSync } = require('zlib');
				    str = '\u0001gzip\u0001' +
					gzipSync(str).toString('base64');
				}
				let s = Buffer.byteLength(str, 'utf8');
				//let text = div_text.innerText; // 备份
				if (s > 40000 && Pal[id].IP && Pal[id].port) {
				    div_text.innerText =
					' Error: 输出结果太长(' + s +
					'字节)。40000 字节以内才传回。<br>';
				} else
				    div_text.innerText = str;
				if (Pal[id].IP && Pal[id].port) {
				    button_send.disabled = true;
				    button_send.onclick();
				    button_send.disabled = false;
				} else {
				    button_mail.disabled = true;
				    button_mail.onclick();
				    button_mail.disabled = false;
				}
				//div_text.innerText = text; // 恢复
			});
		    }
		}
		let r = '';

		//附件格式：
//fileID:filename:size:mtime:fileattr[:extend-attr=val1[,val2...][:extend-attr2=...]]:\a:fileID...
//其中 size, mtime 和 fileattr 是 16 进制格式。文档说，
//If a filename contains ':', please replace with "::".
//但注意冒号是 Windows 非法文件名字符。

		if ((msg.cmd & IPMSG_FILEATTACHOPT) && t[1] && t[1] != '') {
		    let s = t[1].split('\u0007');
		    for (let i in s) {
			// 双冒号代表文件名中的一个实际冒号, 但它是非法字符,
			// 因此，用下划线取代它
			let f = s[i].replace(/::/g, '_').split(':');
			let n = f.length - 1; // 最后一项
			if (n < 5)
				continue;
			let o = 0;
			//if (f[0] != i)
			//	continue; // 文件序号不正确
			if (f[n] == undefined || f[n] != '')
				continue; // 结束位置不正确
			if (f[0] == '') // 开头有冒号的情况
			    o = 1; // 设定 offset=1
			let f_id = f[o];
			let name = f[o+1];
			let size = parseInt(f[o+2],16);
			let time = parseInt(f[o+3],16);
			let attr = (parseInt(f[o+4],16) & 0xFF);
			//if (attr != 1 && attr != 2)
			//	continue; // 文件类型不正确
			r += name +
				(attr == 1 ? '【文件】' :
				(attr == 2 ? '〖文件夹〗' :
				('〈未知文件类型: ' + attr + '〉')))
				+ size + ' 字节 <input ' +
				'type="button" value="接收" title=' +
				'"文件将保存在 shared 文件夹中" ' +
				'onclick="window.opener.tcpRecvFile('
				+ "this, '" + id + "', " + msg.SN +
				", " + f_id + ", '" +
				name.replace(/\\/g, '/')
				    .replace(/\'/g, ':q')
				    .replace(/[<\|>\"\?\*]/g, '_')
				+ "', " +
				size + ", " + time + ", " + attr +
				');"/>&nbsp;<input type='
				+ '"button" value="打开" style=' +
				'"display:none" />&nbsp;<input type="button"'
				+ ' value="打开所在文件夹" style=' +
				'"display:none" /><br/>';
		    }
		    r = ((! Pal[id].haskey && GATEWAYs &&
			GATEWAYs.split(',').indexOf(Pal[id].IP) >= 0) ?
			('<br/><span style="color:red">对方 ' +
			Pal[id].nick + '(' + id +
			') 使用了经过网关周转的 IP 地址 ' +
			Pal[id].IP + ':' + Pal[id].port +
			' 来发送文件, 但当您用 tcp 协议接收时会失败!!<br/> ' +
			'您可以先刷新好友，然后双击对方的内网 IP 与对方重新' +
			'建立会话，并让对方用这个新的会话窗口重新发送文件。' +
			'</span>') :
			((! Pal[id].haskey && Pal[id].port != 2425) ? 
			('<br/><span style="color:red">对方 ' +
			Pal[id].nick + '(' + id +
			') 使用了经过网关周转的 IP 地址 ' +
			Pal[id].IP + ':' + Pal[id].port +
			' 来发送文件, 但当您用 tcp 协议接收时会失败!!<br/> ' +
			'如果对方与您处于同一路由器下，您还有希望用下面的' +
			'办法接收他发来的文件: 您先刷新好友，然后双击对方的' +
			'内网 IP 与对方重新建立会话，并让对方用这个新的会话' +
			'窗口重新发送文件。如果对方与您不在同一路由器下, ' +
			'对方须用 miao 来进行跨路由器通讯。</span>') : '')) +
			'<br/>' + Pal[id].nick + ' 发来 ' + (s.length - 1) +
			' 个附件: <input type="button" value="全部接收" ' +
			'title="文件将保存在 shared 文件夹中"/><br/>' + r;
		}
		let tt = t[0]
			.replace(/^\x02[^\s\0-/:-@[-^\x60\x7b-\x7f]+\x02/, '');
		// 检查是否为 html 格式
		let isHTML = /<(html|head|meta|style|title|body|script|p|table|tr|td|div|span|a)(\s[^<>]*)?>/i.test(tt);
		if (isHTML) {
		    tt = tt
			    .replace(/<\!doctype[^<>]*>/gi, ' ')
			    .replace(/<\!-{2}[\s\S]*?-{2}>/g, ' ')
			    //.replace(/<br\/?>/, '\n')
		    .replace(/<\bscript\b[^<>]*>[\s\S]*?<\/\bscript\b>/gi, ' ')
		    .replace(/<\bstyle\b[^<>]*>[\s\S]*?<\/\bstyle\b>/gi, ' ')
		    .replace(/<\/?html(\s[^<>]*)?>/g, ' ')
		    .replace(/<\/?head(\s[^<>]*)?>/g, ' ')
		    .replace(/<\/?meta(\s[^<>]*)?>/g, ' ')
		    .replace(/<\/?body(\s[^<>]*)?>/g, ' ')
		    .replace(/<\/?title(\s[^<>]*)?>/g, ' ');
		} else {
		    tt = tt
			.replace(Url, function (match) {
			    return '<input type="button" value="'
				+ match + '" title="' +
				'点击按钮将调用系统默认浏览器打开此网址" ' +
				'onclick="window.opener.openExternal('
				+ "'" + match + "'" + ');"/>';
			})
			.replace(/\s*\n/g, '<br/>');
		}
//			.replace(/(<[^<>]*)(ht)(tp)(s?):([^<>]*>)/gi,
//				'$1$2\0$3$4:$5')
//			.replace(/(<[^<>]*)(f)(tp):([^<>]*>)/gi,
//				'$1$2\0$3:$4')
//			.replace(/(<[^<>]*)(s)(mb):([^<>]*>)/gi,
//				'$1$2\0$3:$4')
//			.replace(/(<[^<>]*)(fi)(le):([^<>]*>)/gi,
//				'$1$2\0$3:$4')
//			.replace(Url, function (match) {
//			    return '<input type="button" value="'
//				+ match + '" title="' +
//				'点击按钮将调用系统默认浏览器打开此网址" ' +
//				'onclick="window.opener.openExternal('
//				+ "'" + match + "'" + ');"/>';
//			})
//			.replace(/(<[^<>]*)(ht)\0(tp)(s?):([^<>]*>)/gi,
//				'$1$2$3$4:$5')
//			.replace(/(<[^<>]*)(f)\0(tp):([^<>]*>)/gi,
//				'$1$2$3:$4')
//			.replace(/(<[^<>]*)(s)\0(mb):([^<>]*>)/gi,
//				'$1$2$3:$4')
//			.replace(/(<[^<>]*)(fi)\0(le):([^<>]*>)/gi,
//				'$1$2$3:$4')
//			.replace(/</g, '&lt;')
//			.replace(/>/g, '&gt;')
//			.replace(/\s*\n/g, '<br/>')
//			.replace(/&lt;(\/?)pre&gt;/g, '<$1pre>')
//			.replace(/&lt;(\/?)samp&gt;/g, '<$1samp>')
//			.replace(/&lt;br(\/?)&gt;/g, '<br$1>')
//			.replace(/&lt;(\/?)input((\s.*)?)&gt;/g, '<$1input$2>')
//			.replace(/&lt;(\/?)table((\s.*)?)&gt;/g, '<$1table>')
//			.replace(/&lt;(\/?)tr((\s.*)?)&gt;/g, '<$1tr$2>')
//			.replace(/&lt;(\/?)td((\s.*)?)&gt;/g, '<$1td$2>')
//			.replace(/&lt;(\/?)span((\s.*)?)&gt;/g, '<$1span>')
//			.replace(/&lt;(\/?)div((\s.*)?)&gt;/g, '<$1div>')
//			.replace(/&lt;(\/?)img((\s.*)?)&gt;/g, '<$1img$2>')
//			.replace(/&lt;(\/?)p((\s.*)?)&gt;/g, '<$1p$2>')
//			.replace(/&lt;(\/?)h([r\d](\s.*)?)&gt;/g, '<$1h$2>')
//			.replace(/&lt;(\/?)a((\s.*)?)&gt;/g, '<$1a$2>')
		//if (r)
		//    tt += r;
		if (msg.files) {
		    tt += msg.files
			.replace(/[^\0]+\0/g, function (match) {
			    // 文件名已不含 单引号 等特殊符号
			    let f = match.slice(0, -1);
			    f = path.resolve(path.normalize(f));
			    let ext = path.extname(f).slice(1).toLowerCase();
			    return '<br/>【文件】' + f +
				'&nbsp;<input type="button" value="打开" ' +
				(dangerous.includes(ext)? 'disabled ' : '') +
				'onclick="window.opener.openItem('
				+ "'" + f.replace(/\\/g, '\\\\') + "'" +
				');"/>&nbsp;<input type="button" ' +
				'value="打开所在文件夹" onclick=' +
				'"window.opener.showItemInFolder(' + "'" +
				f.replace(/\\/g, '\\\\') + "'" + ');"/><br/>';
			});
		}
		if (! tt && ! r) // Rexec 口令验证的情形
		    return;
		p.innerHTML =
			'<span style="background-color:#FFDF9F;color:#002060">'
			+ (new Date(Pal[id].time)).toLocaleString
			(date_locale, date_options) + ' ' + Pal[id].nick +
			prompt + '</span><span style="color:#DF5F00"> ' + tt +
			'</span>'; 
		if (r)
		    p.innerHTML += '<span style="color:#DF5F00">'
					+ r + '</span>';
		p.scrollIntoView(false);
		// 对方发来的消息, 不一定保存。需要咱曾经发消息给对方，然后
		// 才会开始保存双方的对话。
		// 检查 history.html 是否存在。不存在时, 不保存此消息。
		save_history(p.innerHTML, id, true);

		// ToDo: 检查 tt 中 html 标签的配对、合法性

		let links = p.querySelectorAll('a');
		for (let i = 0; i < links.length; i++) {
		    let a = links[i];
		    if (! a.href)
			continue;
		    a.title = '在此处点击将调用操作系统的默认浏览器来打开\n' +
				a.href +
				'\n警告! 如果不信任此链接, 请勿点击!';
		    a.onclick = function () {
			openExternal (this.href);
			return false; // 不再打开原链接
		    };
		}

		if (! r)
		    return;

		// 定义【全部接收】按钮的点击事件处理程序
		p.lastChild.firstElementChild.nextElementSibling.onclick =
		    receive_all;

		// 全局自动接收文件
		if (auto_recvfiles && auto_recvfiles.checked) {
		    receive_all ();
		    return;
		}
		// 自动接收好友的文件
		if (Pal[id].haskey > 0) {
		    receive_all ();
		    return;
		}
		// 若存在专用文件夹，则表示曾经接收过，于是自动接收
		let palDirName = Pal[id].nick +
		    id.replace(/@.*$/,'').replace(/:/g,'_');

		let d = path.join('..', 'shared', palDirName);
		if (fs.statSync(d).isDirectory()) {
		    receive_all ();
		    return;
		}

	    } // 局部函数 check_attached_files () 结束

	    if (! Pal[id].haskey || textstr.slice(0, 8) != '\0cipher\0') {
		check_attached_files (':'); // 接收明文
		return;
	    }
	    if (stored_randomID != textstr.slice(8, 24)) {
		fail ('忽略 ' + Pal[id].nick + '(' + id +
			') 发来的无法解密的密文。');
		textstr = '<br/>☹ 密钥丢失, 无法解密 ' + Pal[id].nick +
				'(' + id + ') 发来的密文 ☹ ';
		check_attached_files (':'); // 当作明文来处理
		return; // 忽略无法解密的密文
	    }
	    let ciphertext = Uint8Array.from(atob (textstr.slice(24)),
			c => c.charCodeAt(0));
	    let cipher_length = ciphertext.length;
	    let cleartext = null;
	    let j = 0;
	    for (let i = 0; i < cipher_length; i += 512) {
		window.crypto.subtle.decrypt ( { name: 'RSA-OAEP' },
		    privateKey, ciphertext.slice(i, i + 512))
		.then (function (plaintext) {
		    j += 512;
		    let buf = Buffer.from(new Uint8Array(plaintext));//XP fix
		    if (cleartext)
			cleartext = Buffer.concat([cleartext, buf]);
		    else
			cleartext = Buffer.concat([buf]);
		    if (j < cipher_length)
			return;
		    //textstr = (new TextDecoder('utf-8').decode(cleartext));
		    textstr = iconv.decode(cleartext, 'utf-8');
		    check_attached_files ('#'); // 接收密文
		}).catch (function (err) {
		    fail ('严重错误！解密 ' + Pal[id].nick + '(' + id +
			') 发来的消息失败: ' + err + ' 密文长度=' +
			cipher_length);
		});
	    }
	} // chatWin_onload () 局部函数定义结束

	if (typeof nw === 'object') { // nwjs 的情况
	    if (typeof Pal[id].chatwin !== 'object') {
		nw.Window.open(tmpURL, {id: id}, function (win) {
		    if (typeof Pal[id].chatwin === 'object')
			return;
		    new_win = win;
		    chatWin = win.window;
		    chatWin.pal_id = id;
		    Pal[id].chatwin = chatWin;
		    chatWin.onload = chatWin_onload;
		    let close_handler = function () {
			if (! chatWin.confirm
			    ('若有文件正在传输, 关闭聊天窗将使传输中断。' +
			     '\r\n\r\n确实要关闭与 ' + Pal[id].nick +
			     ' 的对话?')) {
			    setTimeout( () => {
				win.once('close', close_handler);
			    }, 0);
			    return;
			}
			if (Pal[id].chatwin) {
			    Pal[id].chatwin = 0;
			    updateTable(id);
			}
			// win.removeAllListeners('close');
			this.close(true);
		    };
		    win.once('close', close_handler);
		    win.on('closed', function () {
			win = null;
			new_win = null;
			chatWin = null;
			Pal[id].chatwin = 0;
			updateTable(id);
		    });
		});
		return; // 退出 openChat() 函数
	    }
	    // 先前已打开聊天窗, 只需恢复出 chatWin 和 new_win 即可
	    chatWin = Pal[id].chatwin;
	    new_win = nw.Window.get(chatWin);
	    chatWin_onload(); // 处理新消息
	} else { // electron 的情况
	    chatWin = window.open(tmpURL, id,
			'top=60, left=60, width=960, height=600');
	    chatWin.pal_id = id;
	    Pal[id].chatwin = chatWin;
	    chatWin.onload = chatWin_onload;
	    if (tmpURL == '') { chatWin.onload (); }
	}
    } // openChat () 函数定义结束

    function detect () {	// 处理【探测】按钮的点击事件

	who.IP = '255.255.255.255';
	who.port = 2425;

	let detip = window.document.getElementById('detectip');
	let detport = window.document.getElementById('detectport');

	//alert (detip.value);
	//alert (detport.value);
	// 检查 IP 和 port 是否合法
	//.........................
	if (detport.value != '')
		who.port = detport.value;
	if (detip.value == '') {
		sendMsg (Doe, who, IPMSG_BR_ENTRY, Doe.nick);
		return;
	}
	//if (detip.value != '')
	//	who.IP = detip.value;

	//// 试探对方。不知道对方的编码，只能发纯 ascii 消息
	//sendMsg (Doe, who, IPMSG_BR_ENTRY, Doe.nick);
	//// Doe 需要离线吗？如果 ID 启用 MAC，则不需要离线。
	//// sendMsg (Doe, who, IPMSG_BR_EXIT, Doe.nick);
	detectSubnets (detip.value, false);
    }

    // 定义函数，向某个网段发送探测包。

    function detectSubnets (seg, bye, forward) {
	let cmd = (bye ? IPMSG_BR_EXIT : IPMSG_BR_ENTRY);
	//let openPortOnly = false;
	//let mustForward = false;
	//if (who.IP == '') openPortOnly = true;
	//if (who.IP == 'mediator') mustForward = true;
	let ip_v = seg.split(/\//);
	if (! ip_v[0])
	    return;
	if (ip_v.length == 1) {
	    who.IP = seg;
	//    if (openPortOnly)
	//	sendMsg (me, who, IPMSG_ANSENTRY, me.nick);
	//    else if (mustForward)
	//	sendMsg (me, who, IPMSG_BR_ENTRY | (IPMSG_BR_ENTRY << 8),
	//		 me.nick);
	//    else
	    if (forward)
		sendMsg (me, who, (cmd << 8), me.nick + forward);
	    else
		sendMsg (Doe, who, cmd, Doe.nick);
	} else if (ip_v.length == 2) {
	    if (ip_v[1] == '') return;
	    if (isNaN(ip_v[1])) return;
	    let maskbits = parseInt(ip_v[1]);
	    if (maskbits > 32) return;
	    if (maskbits <= 0) return;
	    if (ip_v[1] == '32') {
		who.IP = ip_v[0];
		//if (openPortOnly)
		//    sendMsg (me, who, IPMSG_ANSENTRY, me.nick);
		//else if (mustForward)
		//    sendMsg (me, who, IPMSG_BR_ENTRY | (IPMSG_BR_ENTRY << 8),
		//	     me.nick);
		//else
		if (forward)
		    sendMsg (me, who, (cmd << 8), me.nick + forward);
		else
		    sendMsg (Doe, who, cmd, Doe.nick);
		return;
	    }
	    let num = 0;
	    let aNum = ip_v[0].split('.');
	    if (aNum.length != 4) return;
	    for (let i = 0; i < 4; i++) {
		if (isNaN(aNum[i])) return;
	    	let num_i = parseInt(aNum[i]);
		if (isNaN(num_i)) return;
		if (num_i < 0) return;
		if (num_i > 255) return;
		num += num_i << (8 * (3 - i));
	    }
	    num >>>= 0;	// 确保非负数
//	    if (maskbits < 20) {
//		maskbits = 20;
//	    }
	    let probe = function (ip, end, reverse) {
		//fail ('发往 ' + ip.toString(16));
		setTimeout ( () => {
		//    if (openPortOnly)
		//	sendMsg(me, ip, IPMSG_ANSENTRY, me.nick);
		//    else if (mustForward)
		//	sendMsg(me, ip, IPMSG_BR_ENTRY | (IPMSG_BR_ENTRY << 8),
		//			me.nick);
		//    else
		    if (forward)
			sendMsg(me, ip, (cmd << 8), me.nick + forward);
		    else
			sendMsg(Doe, ip, cmd, Doe.nick);

		    if (reverse) {
			if (ip > end)
			    probe (ip - 1, end, reverse);
			//else
			//    fail ('逆结束 ' + ip.toString(16));
		    } else {
			if (ip < end)
			    probe (ip + 1, end, reverse);
			//else
			//    fail ('结束于 ' + ip.toString(16));
		    }
		}, 15);
	    };

	    //fail ('起始于 ' + num.toString(16));
	    probe (num, num);
	    // 按照与 num 由近及远的顺序进行探测
	    for (let mask = 31; mask >= maskbits; mask--) {
		let r = (1 << (31 - mask));
		let reverse = (num & r);
		let start;
		let end;
		if (reverse) {
		    start = (num ^ r) | (r - 1);
		    end   = (num ^ r) & (-r);
		    start >>>= 0;	// 确保非负数
		    end   >>>= 0;	// 确保非负数

		    //fail ('逆起始 ' + start.toString(16));
		} else {
		    start = (num | r) & (-r);
		    end   = (num | r) | (r - 1);
		    start >>>= 0;	// 确保非负数
		    end   >>>= 0;	// 确保非负数

		    //fail ('起始于 ' + start.toString(16));
		}
		probe (start, end, reverse);
	    }

	}
    } // detectSubnets () 函数定义结束

    let SendMail_callback;

    // 定义函数，邮寄
    function SendMail (id, cmd, str, cb) {
	if (! effective_mailuser || ! effective_mailpass) {
	    fail ('咱没有邮箱或没有口令，无法邮寄。');
	    return;
	}
	if (! navigator.onLine)
	{
	    fail ('咱没有上网，无法邮寄。请检查网络设置。');
	    return;
	}
	let recipient = Pal[id].version.split('#')[8];
	if (! recipient) {
	    fail ('对方无邮箱，无法邮寄。');
	    return;
	}
//	fail ('即将寄往 ' + recipient +
//		((/@/.test(recipient))? '' : '@ethereal.email')
//		+ ', 邮件内容: <br/>(' + str.length + ')' +
//		JSON.stringify(str));

	let ethereal = /@ethereal\.email$/.test(effective_mailuser);
	if (ethereal && (/@/.test(recipient))) {
	    fail ('咱用 ethereal 邮箱无法寄往对方普通邮箱 ' + recipient +
		' 。咱需要用普通邮箱才能寄给对方。');
	    return;
	}

	// return; // 调试时用于临时屏蔽掉 SendMail 函数

	// 如果前一个子进程还未结束，复用它会出现混乱，原因未知
	if (sendMail != null) {
	    fail ('前一个邮寄子进程未结束, 等待 10 秒。');
	    // 异步执行, 10 秒后再试一次
	    setTimeout(SendMail, 10000, id, cmd, str, cb);
	    return;
	}
	create_child_sendMail ();	// 如果没有子进程，创建它
	if (sendMail == null) {
	    // 创建子进程失败，退出
	    fail ('！！严重错误！！创建邮寄子进程时失败！');
	    return;
	}

	// 设定回调函数，以便在子进程 SendMail 成功后执行。
	SendMail_callback = cb;

	// 通知子进程邮寄
	sendMail.send ({
		user: effective_mailuser,
		pass: effective_mailpass,
		smtphost: ethereal ? 'smtp.ethereal.email' :
			(stored_smtphost ? stored_smtphost :
			'smtp.' + effective_mailuser.replace(/^.*@/, '')),
		smtpport: ethereal ? 465 : (stored_smtpport ?
			stored_smtpport : 587),
		smtpsecure: ethereal ? 1 : (smtp_secure.checked ? 1 : 0),
		smtprejectunauth:
			ethereal ? 0 : (smtp_rejectunauth.checked ? 1 : 0),
		smtppass: ethereal ? effective_mailpass : (stored_smtppass ?
			stored_smtppass : effective_mailpass),

		sender: stored_randomID + ' ' + me.nick + ' ' + WGROUP, 
		recipient: recipient,
		subject: (Pal[id].version.split('#')[2].length != 16) ?
			 str.slice(0, 60).replace(/[\r\n].*$/, '') :
			 (stored_randomID + ':' + cmd + ':' + me.nick + ':'
			 + WGROUP + ':' // + Pal[id].yourIP
			 + ':' // + Pal[id].yourPort
			 + ':' + login_name + ':' + me.host
			 + ':' + GUAs.replace(/:/g, '-')),
			 // 废弃 yourIP 和 yourPort，其一是因为在 IPv6 的情形,
			 // 其占用的字节数太多; 其二也是为新增的 GUAs 字段腾出
			 // 空间。
		str: str,
		run: 'SendMail'
	});

    } // SendMail () 函数定义结束

    const IPMSG_NOOPERATION	= 0x00; // 无操作
    const IPMSG_BR_ENTRY	= 0x01; // 用户上线（启动时广播）
    const IPMSG_BR_EXIT		= 0x02; // 用户退出（结束时广播）
    const IPMSG_ANSENTRY	= 0x03; // 通报在线
    const IPMSG_BR_ABSENCE	= 0x04; // 变更无人值守模式, 对方收到
    const IPMSG_BR_NOTIFY	= 0x04; //     后不回复 ANSENTRY 消息
    const IPMSG_BR_ISGETLIST	= 0x10; // 探测可发送主机列表的成员
    const IPMSG_OKGETLIST	= 0x11; // 通知可发送主机列表
    const IPMSG_GETLIST		= 0x12; // 请求发送主机列表
    const IPMSG_ANSLIST		= 0x13; // 发送主机列表
    const IPMSG_ANSLIST_DICT	= 0x14;
    const IPMSG_BR_ISGETLIST2	= 0x18;

    const IPMSG_SENDMSG		= 0x20; // 发送信件
    const IPMSG_RECVMSG		= 0x21; // 确认收到信件
    const IPMSG_READMSG		= 0x30; // 通知开封
    const IPMSG_DELMSG		= 0x31; // 通知丢弃信件
    const IPMSG_ANSREADMSG	= 0x32; // 确认开封（8版开始追加）
    const IPMSG_GETINFO		= 0x40; // 获取 IPMsg 版本信息
    const IPMSG_SENDINFO	= 0x41; // 响应 IPMsg 版本信息
    const IPMSG_GETABSENCEINFO	= 0x50; // 获取无人值守信息
    const IPMSG_SENDABSENCEINFO	= 0x51; // 响应无人值守信息
    const IPMSG_GETFILEDATA	= 0x60; // 请求通过 TCP 发送附件
    const IPMSG_RELEASEFILES	= 0x61; // 丢弃附件
    const IPMSG_GETDIRFILES	= 0x62; // 请求发送文件夹
    const IPMSG_DIRFILES_AUTH	= 0x63;
    const IPMSG_DIRFILES_AUTHRET= 0x64;
    const IPMSG_GETPUBKEY	= 0x72; // 获取 RSA 公钥
    const IPMSG_ANSPUBKEY	= 0x73; // 响应 RSA 公钥

    const IPMSG_AGENT_REQ	= 0xA0;
    const IPMSG_AGENT_ANSREQ	= 0xA1;
    const IPMSG_AGENT_PACKET	= 0xA2;
    const IPMSG_AGENT_PROXYREQ	= 0xA3;

    const IPMSG_DIR_POLL	= 0xB0; // 成员存在通知给成员主控
    const IPMSG_DIR_POLLAGENT	= 0xB1; // 成员母版代理任命
    const IPMSG_DIR_BROADCAST	= 0xB2; // 来自成员主控的代理广播请求
    const IPMSG_DIR_ANSBROAD	= 0xB3; // 向成员主控的代理广播响应
    const IPMSG_DIR_PACKET	= 0xB4; // 成员主控列表更改通知
    const IPMSG_DIR_REQUEST	= 0xB5; // 来自成员主控的代理分组中介请求
    const IPMSG_DIR_AGENTPACKET	= 0xB6; // 来自代理的主仲裁包
    const IPMSG_DIR_EVBROAD	= 0xB7;
    const IPMSG_DIR_AGENTREJECT	= 0xB8;

    const IPTUX_SENDMSG		= 0xFB;
    const IPTUX_SENDSIGN	= 0xFC;
    const IPTUX_SENDSUBLAYER	= 0xFD;
    const IPTUX_SENDICON	= 0xFE;
    const IPTUX_ASKSHARED	= 0xFF;

    // 选项标志类（cmd 高 24 位）

    // 通用选项

    const IPMSG_ABSENCEOPT	= 0x00000100; // 缺席
    const IPMSG_SERVEROPT	= 0x00000200; // 服务
    const IPMSG_DIALUPOPT	= 0x00010000; // 逐个发送上线广播
    const IPMSG_FILEATTACHOPT	= 0x00200000; // 附件
    const IPMSG_ENCRYPTOPT	= 0x00400000; // 加密
    const IPMSG_UTF8OPT		= 0x00800000; // 使用 utf8
    const IPMSG_CAPUTF8OPT	= 0x01000000; // 有能力使用 utf8
    const IPMSG_ENCEXTMSGOPT	= 0x04000000; // 加密扩展消息
    const IPMSG_CLIPBOARDOPT	= 0x08000000; // 剪贴板
    const IPMSG_CAPFILEENC_OBSLT= 0x00001000; // 有文件加密的能力(废弃)
    const IPMSG_CAPFILEENCOPT	= 0x00040000; // 有文件加密的能力
    const IPMSG_CAPIPDICTOPT	= 0x02000000;
    const IPMSG_DIR_MASTER	= 0x10000000;
    const IPMSG_FLAG_RESV1	= 0x20000000;
    const IPMSG_FLAG_RESV2	= 0x40000000;
    //const IPMSG_FLAG_RESV3	= 0x80000000; 

    // 发送消息的选项
    const IPMSG_SENDCHECKOPT	= 0x00000100;
    const IPMSG_SECRETOPT	= 0x00000200;
    const IPMSG_BROADCASTOPT	= 0x00000400;
    const IPMSG_MULTICASTOPT	= 0x00000800;
    const IPMSG_NOPOPUPOPT	= 0x00001000;
    const IPMSG_AUTORETOPT	= 0x00002000;
    const IPMSG_RETRYOPT	= 0x00004000;
    const IPMSG_PASSWORDOPT	= 0x00008000;
    const IPMSG_NOLOGOPT	= 0x00020000;
    const IPMSG_NEWMUTIOPT	= 0x00040000;
    const IPMSG_NOADDLISTOPT	= 0x00080000;
    const IPMSG_READCHECKOPT	= 0x00100000;
    //const IPMSG_SECRETEXOPT	= (IPMSG_READCHECKOPT|IPMSG_SECRETOPT);

    // 附件中的文件类型

    const IPMSG_FILE_REGULAR	= 0x00000001;
    const IPMSG_FILE_DIR	= 0x00000002;
    const IPMSG_FILE_RETPARENT	= 0x00000003;
    const IPMSG_FILE_SYMLINK	= 0x00000004;
    const IPMSG_FILE_CDEV	= 0x00000005; // UNIX 字符设备
    const IPMSG_FILE_BDEV	= 0x00000006; // UNIX 块设备
    const IPMSG_FILE_FIFO	= 0x00000007; // UNIX 管道设备
    const IPMSG_FILE_RESFORK	= 0x00000010; // Mac
    const IPMSG_FILE_CLIPBOARD	= 0x00000020; // Windows 剪贴板

    // 附件中的文件属性选项
    const IPMSG_FILE_RONLYOPT	= 0x00000100; // 只读
    const IPMSG_FILE_HIDDENOPT	= 0x00001000; // 隐藏
    const IPMSG_FILE_EXHIDDENOPT= 0x00002000; // 隐藏, 用于苹果 MacOS X
    const IPMSG_FILE_ARCHIVEOPT	= 0x00004000; // 归档
    const IPMSG_FILE_SYSTEMOPT	= 0x00008000; // 系统

    // 附件中的扩展属性类型
    const IPMSG_FILE_UID	=	0x01;
    const IPMSG_FILE_USERNAME	=	0x02;	// uid by string
    const IPMSG_FILE_GID	=	0x03;
    const IPMSG_FILE_GROUPNAME	=	0x04;	// gid by string
    const IPMSG_FILE_CLIPBOARDPOS=	0x08;	// 
    const IPMSG_FILE_PERM	=	0x10;	// for UNIX
    const IPMSG_FILE_MAJORNO	=	0x11;	// for UNIX devfile
    const IPMSG_FILE_MINORNO	=	0x12;	// for UNIX devfile
    const IPMSG_FILE_CTIME	=	0x13;	// for UNIX
    const IPMSG_FILE_MTIME	=	0x14;	// 修改时间
    const IPMSG_FILE_ATIME	=	0x15;	// 访问时间
    const IPMSG_FILE_CREATETIME	=	0x16;	// 创建时间
    const IPMSG_FILE_CREATOR	=	0x20;	// for Mac
    const IPMSG_FILE_FILETYPE	=	0x21;	// for Mac
    const IPMSG_FILE_FINDERINFO	=	0x22;	// for Mac
    const IPMSG_FILE_ACL	=	0x30;
    const IPMSG_FILE_ALIASFNAME	=	0x40;	// alias fname

    // IPMSG_ABSENCEOPT 缺席模式（在成员识别系统命令中使用）
    // IPMSG_SERVEROPT 服务器（预约）
    // IPMSG_DIALUPOPT 分别发送成员识别系统的命令
    // IPMSG_SENDCHECKOPT 发送检查
    // IPMSG_SECRETOPT 封条
    // IPMSG_READCHECKOPT 确认封条（8版开始追加）
    // IPMSG_PASSWORDOPT 锁头
    // IPMSG_BROADCASTOPT 广播（广播）
    // IPMSG_MULTICASTOPT 组播（多选）
    // IPMSG_NEWMULTIOPT 新版本组播（预约）
    // IPMSG_NOLOGOPT 不留在日志中（建议）
    // IPMSG_NOADDLISTOPT 未进行 BR_ENTRY 的临时成员通知
    // IPMSG_AUTORETOPT 自动响应（用于防止乒乓球）
    // IPMSG_FILEATTACHOPT 文件附件
    // IPMSG_ENCRYPTOPT 密码
    // IPMSG_ENCEXTMSGOPT 在密文中包含文件附件信息和收件人信息
    // IPMSG_CAPUTF8OPT 有使用 UTF-8 的能力
    // IPMSG_UTF8OPT 信件整体使用UTF-8
    // IPMSG_CLIPBOARDOPT 支持消息图像嵌入附件
    // IPMSG_CAPFILEENCOPT 文件附件请求&数据加密支持
    // IPMSG_ENCFILEOPT 文件附件数据的加密请求
    // IPMSG_CAPIPDICTOPT 支持IPDict形式
    // IPMSG_DIR_MASTER 成员母版
    // IPMSG_RETRYOPT 重发标志（获取HOSTLIST时使用）

    // 密码扩展标志（在hex表示中使用扩展部分的组合）

    // IPMSG_RSA_512	  = 0x00000001; 公钥（RSA 512比特）加密能力
    // IPMSG_RSA_1024	  = 0x00000002; 公钥（RSA 1024比特）加密能力
    // IPMSG_RSA_2048	  = 0x00000004; 公钥（RSA 2048比特）加密能力
    // IPMSG_RC2_40	  = 0x00001000; 通用密钥（RC2 40比特）加密能力
    // IPMSG_RC2_128	  = 0x00004000; 通用密钥（RC2 128比特）加密能力
    // IPMSG_RC2_256	  = 0x00008000; 通用密钥（RC2 256比特）加密能力
    // IPMSG_BLOWFISH_128 = 0x00020000; 公共密钥(Blowfish 128位)加密能力
    // IPMSG_BLOWFISH_256 = 0x00040000; 公共密钥(Blowfish 256位)加密能力
    // IPMSG_AES_256 公共密钥（AES 256比特）加密能力
    // IPMSG_PACKETNO_IV 在公钥加密IV中使用Packet号码字符串
    // IPMSG_ENCODE_BASE64 在密码句的公共密钥以后的记述中使用base 64
    // IPMSG_SIGN_SHA1 在明文中赋予SHA1电子签名
    // IPMSG_SIGN_SHA256 在明文中赋予SHA256电子签名
    // IPMSG_NOENC_FILEBODY 文件传送的主体部分不使用公共密钥加密
    // IPMSG_SIGN_MD5	  = 0x10000000;

    //	当数据接收端下载文件夹附件时，使用 IPMSG_GETDIRFILES 命令, 输入
    //	packetID:fileID 到扩展区域, 发送一个数据传输请求包(都是 hex 格式)。
    //	
    //	数据发送端发送以下文件夹数据格式:
    //	header-size:filename:file-size:fileattr[:extend-attr=val1
    //	[,val2...][:extend-attr2=...]]:contents-data
    //	下一个 headersize:下一个 filename...
    //	 （除了 filename 和 contens-data 以外全都是十六进制格式）
    //	header_size 表示从 header_size 的开头到 contents-data 之前的“:”
    //	的长度。extend-attr 可有可无，也可存在多个扩展属性。在“=”之后输入
    //	对应的数据值。
    //	
    //	When fileattr is IPMSG_FILE_DIR, IPMsg recognizes that it is
    //	automatically in the directory, the next file data is after the
    //	directory.
    //	
    //	当 fileattr 是 IPMSG_FILE_DIR 时，IPMSG 会自动识别它在目录中，下一个
    //	文件数据在该目录之后。(如果 fileattr 是 IPMSG_FILE_DIR，则会自动将其
    //	视为隐藏在该目录中的状态，继续后续的文件信息。)
    //	
    //	当 fileattr 是 IPMSG_FILE_RETPARENT 时，表示返回父目录; 此时文件名总
    //	是“.”，而属性值则表示返回父目录之前的当前目录信息。
    //	
    //	发送过程直接从附件目录本身开始，最后用 IPMSG_FILE_RETPARENT 命令发送
    //	返回附件目录的信息，告知结束。
    //	
    //	另外，在 ENTRY 系的数据包上设置 IPMSG_FILEATTACHOPT 标志，表示可支持
    //	文件附件。
    //	
    //	（附件名加密）在将上述格式内容从邮件正文末尾持续记述为“\0”后，可以对
    //	整个信件进行加密。此时，将 IPMSG_ENCEXTMSGOPT 设置在 Entry 系数据包和
    //	IPMSG_SENDMSG 上。
    //	
    //	（修改附件后文件长度改变）普通文件也可以使用 IPMSG_GETDIRFIES，
    //	这样，即使文件大小改变了，也能适应。但是，IPMSG_FILE_CLIPBOARD 表示的
    //	是内存文件，不会变更文件大小，所以只支持 IPMSG_GETFILEDATA。
    //	
    //	多语言（UTF-8）扩展
    //	
    //	To indicate a capability of UTF-8 message, use IPMSG_CAPUTF8OPT in
    //	IPMSG_BR_ENTRY/IPMSG_ANS_ENTRY/IPMSG_BR_ABSENCE command.
    //	Using UTF-8 message, use IPMSG_UTF8OPT in IPMSG_SENDMSG or etc.
    //	But IPMSG_BR_ENTRY/IPMSG_BR_EXIT/IPMSG_BR_ABSENCE must not use
    //	IPMSG_UTF8OPT, for backward compatibility (old clients).
    //	
    //	主机可以用 UTF-8 来进行多国语言通信，收发包含文件收发信息的消息。
    //	若用 IPMSG_BR_ENTRY / IPMSG_ANS_ENTRY / IPMSG_BR_ABSENCE 命令发送
    //	设置了 IPMSG_CAPUTF8OPT 的数据包，即表明可以通过 UTF-8 进行收发。
    //	 
    //	如果双方都支持 IPMSG_CAPUTF8OPT，可以在设置 IPMSG_UTF8OPT 后，
    //	发送用 UTF-8 表示的信息包（包括文件附件的消息部分）。
    //	
    //	接收附件时，如果在原始附件中设置了 IPMSG_UTF8OPT，用同样设置了
    //	IPMSG_UTF8OPT 的 IPMSG_GETFILEDATA 命令发送附件传送请求，可以
    //	接收用 UTF-8 编码表示的文件名/目录名数据。
    //	
    //	对于 BR 系列的数据包 IPMSG_BR_ENTRY, IPMSG_BR_EXIT, IPMSG_BR_ABSENCE,
    //	不能试图用 IPMSG_UTF8OPT 来实现 UTF-8（因为现有的非 UTF-8 客户端会出
    //	问题）。
    //	 
    //	因此，在 BR 系列数据包中，在组名扩展的末尾之后，添加 \0\n 两个字节
    //	（在不存在组名扩展的情形，则插入 \0 作为伪组名，然后继续添加 \0\n。
    //	也就是说，合起来成为 \0\0\n）之后，以 UTF-8 格式添加以下条目。
    //	
    //	UN:用户名（utf-8）\n
    //	HN:主机名（utf-8）\n
    //	NN:昵称（utf-8）\n
    //	GN:组名（utf-8）\n
    //	
    //	另外，可以省略只有 ASCII 字符的项目或不存在的项目。
    //	如果上述扩展和常规字段不同，则扩展项将被解释为正确的。

    let send_SN = 0;
    let SendFile = {};
    let RecvMsgCBs = {}; // RECVMSG callbacks

    let check_miao_reply_time = 0;

    function check_miao_reply (time) {

	if (UPnP_WAN_IP)
	    return true;
	for (let id in Pal) {
	    if (/^[0-9A-Fa-f]{16}$/.test(id)) // IPv6 user
		continue;
	    if (Pal[id].time <= (time ? time : check_miao_reply_time))
		continue;
	    // 设置 ip 为对方看到的我的本机 IP
	    let ip = Pal[id].yourIP;
	    if (! ip) continue; // 对方不是 miao 用户, 无法获取有用信息, 跳过
	    if (/:/.test(ip)) continue; // IPv6, 跳过

	    // 如果 ip 不是局域网地址, 它一定是路由器外网地址。
	    //		注意对方能看到我的 IP 为外网地址, 说明对方也在外网,
	    //		而且, 我的这个外网地址一定是路由器转发出去的地址。
	    // 否则,
	    //		设置 ip 为我看到的对方 IP,
	    //		如果 ip 不是局域网地址, 则一定是路由器外网地址。
	    //			注意此时对方看到的我的 IP 是在局域网,
	    //			对方本身的 IP 又是在外网,
	    //			说明对方与我的本机是处于同一局域网,
	    //			只不过对方还有一个外网的身份(由DMZ映射引起)。
	    //		否则,
	    //			设置 ip 为对方多个 IP 地址中的最后一个 IP,
	    //			也就是对方已经获取到的外网 IP。如果这个 ip
	    //			不是局域网地址, 它一定是路由器外网地址。
	    //			注意此时对方看到的我的 IP 是在我的局域网中,
	    //			而我看到的对方 IP 也是在我的局域网中。
	    //			这说明对方与我处于同一局域网中，
	    //			因而对方所获得的外网 IP, 也就是我的外网 IP。
	    //			这个判断依赖对方的判断, 因此不一定准确。

	    if (! checkCIDR (ip) || ! checkCIDR (ip = Pal[id].IP)) {
		// 这里捕捉到的 ip 是准确可靠的路由器外网 IP
		if (stored_outer_LAN_IP &&
		    stored_outer_LAN_IP.replace (/\/.*$/, '') == ip) {
			return true;
		}
		fail ('分析得到外网 IP: ' + ip);
		let outLanIP = window.document.getElementById('outer_LAN_IP');
		outLanIP.value = ip;
		outLanIP.onchange ();
		return true;
	    } else if ((ip = Pal[id].myIPs.replace (/^.*,,/, '')) && 
	    		(ip != Pal[id].myIPs) && (! checkCIDR (ip))) {
		// 这里捕捉到的 ip 是对方给出的路由器外网 IP, 不一定准确。
		// 因此只能作为待定值, 而不能立即采纳。

		// 但是, 我们现在向这个 ip 发送探测消息,
		// 促使它返回有效信息而被前面那个 if 条件捕捉到,
		// 当然是在下次执行本函数的时候才会捕捉到。
		// 这里假定路由器设置了 DMZ, 并且 DMZ 主机能够正常应答。
		// 如果 DMZ 主机不工作（关机或休眠）, 所有的探测都将失败。

		// 这次发送的 ip 是外网地址, 对方的响应地址也是外网地址,
		// 因此, 不会再次被本 if 条件捕捉到, 因而不会造成无限循环。

		// 输出调试信息, 看看有没有异常的无限循环情况发生
		fail ('(监控可能的无限循环)待定的外网 IP: ' + ip);

		who.IP = ip;
		sendMsg (Doe, who, IPMSG_BR_ENTRY, Doe.nick);

		// 200 毫秒以后检查一次
		let time = Date.now ();
		setTimeout (check_miao_reply, 200, time);

		// 此处忽略探测到的待定值, 而不是用 return true 来直接承认
	    }
	}
	return false;
    }

    // 函数定义，刷新好友列表

    function refresh (bye, forward) {

	if (! udp_socket_opened)
	    return;
	if (! navigator.onLine)
	    return;
	updateMyNetInfo();
	updateTable(); // refresh 函数不是频繁执行, 因此不会造成无限循环
	if (! ADDRs)
	    return;
	if (! CIDRs)
	    return;
	if (! bye)
	    setTimeout(dir_sync_start, 30000);

	let cmd = (bye ? IPMSG_BR_EXIT : IPMSG_BR_ENTRY);

	// 向全网发送广播。不知道对方字符集编码，只能发纯 ascii 消息
	all.IP = '255.255.255.255';
	if (forward) {
	    sendMsg (me, all, (cmd << 8), me.nick + forward);
	    return; // 转发时, 只向全网广播一次, 不向各网卡定向广播
	}
	else
	    sendMsg (Doe, all, cmd, Doe.nick);
	// 为了快速返回，设置异步执行!
	if (bye && ! forward) // 但在退出 miao 时, 同步通告离线
	    refresh2 (bye, forward);
	else
	    setTimeout(refresh2, 0, bye, forward);

    } // refresh () 函数定义结束

    function refresh2 (bye, forward) {

	let cmd = (bye ? IPMSG_BR_EXIT : IPMSG_BR_ENTRY);

	// 对本机各网卡分别进行定向广播
	//updateMyNetInfo (); // 可能切换了 WiFi 热点，必须更新网络参数
	var cidr = CIDRs.split(',');
	for (let i in cidr) {
		
		let addr_bits = cidr[i].split('/');
		let addr = addr_bits[0];
		let bits = addr_bits[1];
		let num = 0;
		let aNum = addr.split('.');
		if (aNum.length != 4) {
			alert ('Fatal: Invalid IPv4 address: ' + CIDRs);
			return;
		}
		num += parseInt(aNum[0]) << 24; 
		num += parseInt(aNum[1]) << 16; 
		num += parseInt(aNum[2]) << 8; 
		num += parseInt(aNum[3]) << 0; 
		//num >>>= (32 - bits);
		num |= (1 << (32 - bits)) - 1; // 得到定向广播地址

		// 把定向广播地址转换成 x.x.x.255 的形式
		all.IP = ((num >> 24) & 0xFF) + '.' +
			 ((num >> 16) & 0xFF) + '.' +
			 ((num >> 8) & 0xFF) + '.' +
			 (num & 0xFF);
		// 向该网卡所在网络发送定向广播
		//sendMsg (Doe, all, cmd, Doe.nick);
		if (forward)
			sendMsg (me, all, (cmd << 8), me.nick + forward);
		else
			sendMsg (Doe, all, cmd, Doe.nick);
	}
	all.IP = '255.255.255.255'; // 恢复其默认值

	// 本机 GUA 地址
	let host = GUAs.split(',');
	for (let i = 0; i < host.length; i++) {
	    if (! host[i])
		continue;
	    who.IP = host[i].replace(/\/\d+$/, '');
	    who.port = 2425;
	    sendMsg (me, who, cmd, me.nick);
	}
	who.IP = '255.255.255.255';
	who.port = 2425;
	// 为了快速返回，设置异步执行!
	if (bye && ! forward) // 但在退出 miao 时, 同步通告离线
	    refresh3 (bye, forward);
	else
	    setTimeout(refresh3, 0, bye, forward);

    } // refresh2 () 函数定义结束

    function refresh3 (bye, forward) {

	// 向路由器外网发送消息。同样只能发纯 ascii 消息
	if (! bye)
	if (! forward)
	if (stored_outer_LAN_IP && ! /:/.test(stored_outer_LAN_IP)) {
	    let broadcast = function () {
		let ip = stored_outer_LAN_IP;
		//if (/^\d+[.]\d+[.]\d+[.]\d+$/.test(ip))
		//	ip += '/22';
		let v = ip.split('.');
		let bits = 0;
		if (v[0] == 192 && v[1] == 168) {
		    bits = 16;
		} else if (v[0] == 172 && v[1] > 15 && v[1] < 32) {
		    bits = 12;
		} else if (v[0] == 10) {
		    bits = 8;
		}
		if (stored_outer_LAN_mask
			&& ! isNaN(stored_outer_LAN_mask)
			&& Number(stored_outer_LAN_mask) >=8
			&& Number(stored_outer_LAN_mask) <= 32)
		{
		    if (stored_outer_LAN_mask > bits)
			ip += '/' + stored_outer_LAN_mask;
		    else
			ip += '/' + bits;
		}
		if (bits)
		{
		    detectSubnets (ip, bye, forward);
		}

		// 探测外网, 10 秒后检查是否获取外网 IP
		check_miao_reply_time = Date.now ();
		setTimeout (check_miao_reply, 10000);
	    };

	    let ip = stored_outer_LAN_IP.replace(/\/.*$/, '');
		// 确认这个 IP 是路由器外网 IP
		// 向它发探测消息, 看有没有回应

		// 确定 DMZ 主机是否有响应, 如果无响应, 设定本机为 DMZ

	    let tmp_time = Date.now(); // 先记录当前时间

	    let check_ip = function () {

		// 向 ip 发探测消息
		who.IP = ip;
		sendMsg (Doe, who, IPMSG_BR_ENTRY, Doe.nick);

		setTimeout(() => {
			for (let id in Pal) {
			    if (Pal[id].IP != ip) // || Pal[id].port != 2425
				continue;
			    if (Pal[id].time < tmp_time)
				continue;
			    // 主机有响应, 姑且认为 ip 是路由器外网 IP
			    // 若严格检查, 需找到该 DMZ 主机的内网 IP
			    broadcast (); // 刷新外网好友
			    return;
			}

			if (Date.now () - tmp_time < 10000) {
			    check_ip ();
			    return;
			}
			// 这个 ip 不是路由器外网 IP
			//fail ('填写的路由器外网 IP ' + ip + ' 不能正常' +
			//	'响应。可能有如下四种原因: 1. 在 miao 的' +
			//	'设置中填写的外网 IP 不是此路由器的外网 IP;' +
			//	' 2. 路由器未设置 DMZ 主机; 3. 设定的 DMZ ' +
			//	'主机处于关机或休眠状态; 4. 设定的 DMZ 主机' +
			//	'上未运行任何一种飞鸽软件。<br/>如果换用支' +
			//	'持 UPnP 的路由器, 就不需要手动设置了。miao' +
			//	' 未获取外网 IP 的结果是: 无法自动探测路由' +
			//	'器外网好友, 您必须在 miao 的设置中手动添加' +
			//	'外网好友的网段, 才能探测。');
			if (stored_outer_LAN_IP == ip) {
			    let outIP =
				window.document.getElementById('outer_LAN_IP');
			    outIP.value = '';
			    outIP.onchange ();
			}

		}, 200);
	    };
	    let v = ip.split('.');
	    let bits = 0;
	    if (v[0] == 192 && v[1] == 168) {
		bits = 16;
	    } else if (v[0] == 172 && v[1] > 15 && v[1] < 32) {
		bits = 12;
	    } else if (v[0] == 10) {
		bits = 8;
	    }
	    if (bits)
		check_ip ();
	}

	// 向自定义网段每台主机发送消息。同样只能发纯 ascii 消息

	if (! miaoconfig.palSubnets)
		return;

	who.IP = '255.255.255.255';
	who.port = 2425;

	let seg_v = miaoconfig.palSubnets.split(/[\s,;]+/);
	for (let i in seg_v) {
		detectSubnets (seg_v[i], bye, forward);
	}

	//if (bye) return;

	// imapConnect (); // 检查邮件
    } // refresh3 () 函数定义结束

    //const dgram = require('dgram');
    //let udp_socket = dgram.createSocket('udp4');
    let udp_socket = require('dgram').createSocket('udp6');
    let udp_socket_opened = false;
    let tcp_server_opened = false;

    // 函数定义，发送消息

    function sendMsg (From, To, cmd, msg) {
	let id;
	if (typeof To === 'string') {
	    id = To;
	    To = Pal[id];
	} else if (typeof To === 'number') {
	    let ip3 = ((To >> 24) & 255);
	    let ip2 = ((To >> 16) & 255);
	    let ip1 = ((To >> 8) & 255);
	    let ip0 = (To & 255);
	    who.IP = ip3 + '.' + ip2 + '.' + ip1 + '.' + ip0; 
	    who.port = 2425;
	    To = who;
	}

	if (! To.IP || ! To.port) {
	    return false;
	}
	// 本机无 GUA 地址，不能向 GUA 地址发消息
	if (! GUAs && /^[23][0-9A-Fa-f]{3}:/.test(To.IP)) {
	    return false;
	}

	let ip = To.IP;
	let isIPv4 = false;
	if (/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(ip)) {
	    // 将 IPv4 转为 IPv6 格式
	    ip = ip.split('.');
	    if ((Number(ip[0]) | Number(ip[1]) |
		 Number(ip[2]) | Number(ip[3])) < 256) {
		ip = '::ffff:' +
			((ip[0] << 8) | ip[1]).toString(16) + ':' +
			((ip[2] << 8) | ip[3]).toString(16);
		isIPv4 = true;
	    }
	}
	let s;
	let account;

	if (! effective_mailuser || ! effective_mailpass)
	    account = '';
	else
	    account = effective_mailuser
			.replace(/@ethereal\.email$/, '');

	if ((cmd & 255) == IPMSG_SENDMSG)
	    cmd |= (IPMSG_UTF8OPT | IPMSG_CAPUTF8OPT);
	if ((cmd & 255) == IPMSG_ANSENTRY)
	    cmd |= (IPMSG_UTF8OPT | IPMSG_CAPUTF8OPT);
	if ((cmd & 255) == IPMSG_BR_ENTRY)
	    cmd |= IPMSG_CAPUTF8OPT;
	if ((cmd & 255) == IPMSG_BR_EXIT)
	    cmd |= IPMSG_CAPUTF8OPT;
	s = '1_lbt6_0#131200#' + stored_randomID + '#0#0#0#4001#9#' + account +
		':' + send_SN + ':' + From.id + ':' + From.host + ':' + cmd +
		':' + msg + '\0';

	let t;
	let myIPs = (isIPv4 ? (ADDRs + (stored_outer_LAN_IP ?
		    (',,' + stored_outer_LAN_IP.replace(/\/.*$/, '')) : ''))
		    : GUAs);
	if ((cmd & 255) == IPMSG_BR_ENTRY || (cmd & 255) == IPMSG_BR_EXIT) {
	    // 附加字符编码、IP地址等信息
	    //updateMyNetInfo ();
	    s += (From == Doe ? '' : WGROUP) + '\0';
	    t =	'\nUN:' + login_name +
		'\nHN:' + me.host +
		'\nNN:' + me.nick +
		'\nGN:' + WGROUP + // '\n' +
		'\0utf-8\0' + myIPs + '\0' + To.IP + '\0' + To.port + '\0\0' +
		miaoconfig.services
			.replace(/^\s*[\r\n].*$/, '')
			.replace(/[\r\n]\s*[\r\n].*$/, '')
			.replace(/[\r\n]\s*$/, '')
			+ '\0';
		// + To.yourIP + '\0' + To.yourPort + '\0';
	}

	if ((cmd & 255) == IPMSG_ANSENTRY) {
	    // 附加字符编码、IP地址等信息
	    //updateMyNetInfo ();
	    s += WGROUP + '\0\0utf-8\0' + myIPs + '\0' + To.IP + '\0' +
		To.port + '\0';
		// + To.yourIP + '\0' + To.yourPort + '\0';
	    if (To.haskey != 1) {	// 如果没有对方的key，请求对方发送key
		s += 'askKey\0';
		s += miaoconfig.services
			.replace(/^\s*[\r\n].*$/, '')
			.replace(/[\r\n]\s*[\r\n].*$/, '')
			.replace(/[\r\n]\s*$/, '')
			+ '\0';
	    }
	}

	let buf;
	if (To.charset != '' && To.charset != 'utf8') {
	    buf = iconv.encode (s, To.charset);
	    s = Buffer.from (buf);
	    if (t)
		s = Buffer.concat([s, Buffer.from(t)]);
	} else {
	    if (t)
		s += t;
	}
	let ret = true;

	// socket.send(msg[, offset, length][, port][, address][, callback])
	// msg <Buffer> | <Uint8Array> | <string> | <Array> Message to be sent.
	// offset <integer> Offset in the buffer where the message starts.
	// length <integer> Number of bytes in the message.
	// port <integer> Destination port.
	// address <string> Destination hostname or IP address.
	// callback <Function> Called when the message has been sent.

	if (! navigator.onLine)
	{
	    fail ('没有连网，向 ' + To.IP + ' 发消息失败；请检查网络设置。');
	    return false;
	}
	if (! udp_socket_opened) {
	    fail ('udp_socket 处于关闭状态，发消息失败；请检查 2425 ' +
		'端口是否被其它程序占用。');
	    return false;
	}
	try {
	    udp_socket.send(s, To.port, ip, (err) => {
		if (err) {
		    fail ('udp_socket.send() 失败: ' + JSON.stringify(err)
				+ ' ADDRs=' + ADDRs + ' ip=' + ip
				+ ' GUAs=' + GUAs);
		    // 失败时更新本机网络连接信息
		    // 若发现网络信息有变更, 则安排一次刷新(1分钟后执行)
		    if (updateMyNetInfo()) {
			if (refresh_timeoutID)
			    clearTimeout(refresh_timeoutID);
			refresh_timeoutID = setTimeout(refresh, 60000);
		    }
		}
	    });
	} catch (e) {
	    fail ('捕获 udp_socket.send() 异常: ' + e);
	    return false;
	}
	// 若 To.IP 是 IPv6, 且本机有 GUA 地址，且消息类型是上线、下线、应答或
	// 收讫，则使用相同的 send_SN 向 To 的所有 GUA 地址发送消息, 间隔 1 秒
	if (! isIPv4 && GUAs && To.myIPs &&
	    ((cmd & 255) == IPMSG_BR_ENTRY ||
	     (cmd & 255) == IPMSG_BR_EXIT  ||
	     (cmd & 255) == IPMSG_ANSENTRY ||
	     (cmd & 255) == IPMSG_RECVMSG )) {
	    let cidr = To.myIPs.split(',');
	    let port = To.port;
	    for (let i = 0; i < cidr.length;) {
		let addr = cidr[i].split('/')[0];
		i++;
		setTimeout( () => {
		    try {
			udp_socket.send(s, port, addr, (err) => {
			    if (err) {
				fail ('udp_socket.send() 失败: ' +
				    JSON.stringify(err) + ' addr=' + addr);
			    }
			});
		    } catch (e) {
			fail ('捕获 udp_socket.send() 异常: ' + e);
		    }
		}, i * 1000);
	    }
	}

	if (ret) {
	    if ((cmd & 255) == IPMSG_SENDMSG) {
		if (cmd & IPMSG_FILEATTACHOPT) {
		    // 只保留 100 个元素
		    let noErr = true;
		    let c;
		    while ((c = Object.keys(SendFile).length) > 99) {
			delete SendFile[Object.keys(SendFile)[0]];
			if (c <= Object.keys(SendFile).length) {
			    noErr = false;
			    break;
			}
		    }
		    if (noErr)
			SendFile[send_SN] = Pal[id].send.input;
		}
	    }
	    //let time_sec = parseInt(Date.now() / 1000);
	    //if (send_SN < time_sec)
	    //	send_SN = time_sec;
	    //else
		send_SN ++;
	}
	return ret;
    } // sendMsg () 函数定义结束

    // 函数定义，检查 ip 是否为内部局域网地址。如果是,
    // 返回与 ip 相应的本机网卡的如下格式的字符串:
    //		'IP地址/子网掩码,整数形式的子网地址,子网地址'

    function checkCIDR (ip) {
	if (/:/.test(ip)) { // 处理 IPv6 格式的 IPv4 地址
	    if (/^::ffff:[a-fA-F0-9]{1,4}:[a-fA-F0-9]{1,4}$/i.test(ip)) {
		ip = ip.slice(7).split(':');
		ip[0] = parseInt(ip[0], 16);
		ip[1] = parseInt(ip[1], 16);
		ip = (ip[0] >>> 8) + '.' + (ip[0] & 255) + '.' +
		     (ip[1] >>> 8) + '.' + (ip[1] & 255);
	    } else
	    if (/^::ffff:\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/i.test(ip)) {
		ip = ip.slice(7);
	    } else // 纯粹的 IPv6 地址，视为外网地址
		return false;
	}
	//updateMyNetInfo ();
	//if (rinfo.port != 2425) return false;
	if (ip == '127.0.0.1')
	    return ip + '/32,2130706433,' + ip;
	if (! CIDRs)
	    return false;
	let cidr = CIDRs.split(',');
	for (let i = 0; i < cidr.length; i++) {
		let addr_bits = cidr[i].split('/');
		let addr = addr_bits[0];
		let bits = addr_bits[1];
		let num = 0;
		let aNum = ip.split('.');
		if (aNum.length != 4) {
			alert ('Fatal: Invalid IPv4 address: ' + ip);
			return false;
		}
		num += parseInt(aNum[0]) << 24; 
		num += parseInt(aNum[1]) << 16; 
		num += parseInt(aNum[2]) << 8; 
		num += parseInt(aNum[3]) << 0; 
		num >>>= (32 - bits);
		let no2 = 0;
		let aNo2 = addr.split('.');
		if (aNo2.length != 4) {
			alert ('Fatal: Invalid IPv4 address: ' + CIDRs);
			return false;
		}
		no2 += parseInt(aNo2[0]) << 24; 
		no2 += parseInt(aNo2[1]) << 16; 
		no2 += parseInt(aNo2[2]) << 8; 
		no2 += parseInt(aNo2[3]) << 0; 
		no2 >>>= (32 - bits);
		if (num == no2) {
		    num <<= (32 - bits);
		    num >>>= 0;
		    let network = ((num >> 24) & 0xFF) + '.' +
				  ((num >> 16) & 0xFF) + '.' +
				  ((num >> 8) & 0xFF) + '.' +
				  (num & 0xFF);
		    return cidr[i] + ',' + num + ',' + network;
		}
	}
	return false;
    } // checkCIDR () 函数定义结束

    let quit_ongoing = 0;
    let UPnP_M_SEARCH_time = 0;

    function UPnP_M_SEARCH () {
	// M-Search message body
	// 有些软件不用
	//	ST:upnp:rootdevice
	// 而用
	//	ST:urn:schemas-upnp-org:device:InternetGatewayDevice:1
	// 究竟哪个更好? 

	// 仅当本机所有网卡都是 192.168.x.x（C 类地址）时才使用 UPnP 功能。
	if (ADDRs.replace(/192\.168\.\d+\.\d+,{0,1}/g, '')) {
	    return; // 本机有网卡地址属于 C 类以外的大局域网, 退出
	}

	if (Date.now () - UPnP_M_SEARCH_time < 60000 && quit_ongoing != 1)
	    return;

	UPnP_M_SEARCH_time = Date.now ();

	// 可能已重新连接到不支持 UPnP 的路由器, 因此探测前清除上次的探测值 
	UPnP_WAN_IP = null;

	let s = 'M-SEARCH * HTTP/1.1\r\n' +
		'HOST:239.255.255.250:1900\r\n' +
		'ST:upnp:rootdevice\r\n' +
		'MX:2\r\n' +
		'MAN:"ssdp:discover"\r\n\r\n';

	// Send M-Search message to multicast address for UPnP
	udp_socket.send(s, 1900, '::ffff:efff:fffa', //'239.255.255.250',
	    (err) => {
		if (err) {
		    fail ('UPnP 向多播地址发送 M-Search 消息失败: ' + err);
		    debug_info.scrollIntoView (false);
		}
	    }
	);
    }

    function bind_OK_init () {
	// 向局域网好友发送探测消息
	if (! iconv) {
		iconv = require('./node_modules/iconv-lite');

		setTimeout (bind_OK_init, 20);
		return;
	}
	updateMyNetInfo ();
	if (! ADDRs) {
		setTimeout (bind_OK_init, 30000);
		return;
	}

	refresh ();

	// 启动后的自动刷新已完成, 60 秒后检查是否获取外网 IP
	check_miao_reply_time = Date.now ();
	setTimeout (check_miao_reply, 60000);
    }

    function udp_socket_bind_listener () {
	if (udp_socket_opened)
		return;
	udp_socket_opened = true;
	udp_socket.setBroadcast (true);
//	udp_socket.setMulticastTTL (128);
//	udp_socket.addMembership ('229.29.59.79');

	// bind 成功，去除不断尝试 bind 的周期计时操作
	if (udp_socket_IntervalID != undefined) {
	    clearInterval (udp_socket_IntervalID);
	    udp_socket_IntervalID = undefined;
	}
	setTimeout (bind_OK_init, 0);
//	setInterval(() => {
//	    udp_socket.send ('Hi! I am server', 2425, '229.29.59.79');
//	}, 60000);
	lastErrUDP = '';
    } // udp_socket_bind_listener () 函数定义结束
    
    // 定义函数，开启 udp 服务
    function udp_socket_open () {
	if (! navigator.onLine || udp_socket_opened)
		return;
	try {
	    udp_socket.bind (2425, udp_socket_bind_listener);
	} catch (e) {
	    fail (' udp_socket.bind() 严重错误: ' + e);
	}
    } // udp_socket_open () 函数定义结束

    // 定义函数，开启 tcp 服务
    function tcp_server_open () {
	if (! navigator.onLine || tcp_server_opened)
		return;

	const tcp_server = require('net').createServer();

	tcp_server.on ('error', (e) => {
	    let err = e.toString();
	    if (err != lastErrTCP) // 避免重复显示同一条错误信息
		fail (err + ' 请关闭占用 TCP 2425 端口的程序。');
	    lastErrTCP = err;
	    if (tcp_server_opened) {
		tcp_server.close();
		tcp_server_opened = false;
	    }
	});

	tcp_server.on ('close', () => {
	    tcp_server_opened = false;
	    // server 关闭后，启动周期计时器尝试再次打开它
	    // 每隔 10 秒执行一次 tcp_server_open 函数，尝试绑定 2425 端口。
	    if (tcp_server_IntervalID == undefined)
		tcp_server_IntervalID = setInterval (tcp_server_open, 10000);
	});

	tcp_server.on('connection', tcp_connect_listener);

	// 接受 IPv4 和 IPv6 连接, 注释掉 0.0.0.0
	tcp_server.listen (2425, //'0.0.0.0',
	    () => {
		if (tcp_server_opened)
			return;
		tcp_server_opened = true;
		// 启动成功，去除不断尝试启动的周期计时操作
		if (tcp_server_IntervalID) {
		    clearInterval (tcp_server_IntervalID);
		    tcp_server_IntervalID = undefined;
		}
		lastErrTCP = '';
	    }
	);
    } // tcp_server_open () 函数定义结束

    // 邮件接收消息处理

    function recvMail_on_message (msg) {
	    if (msg.event == 'error') {	// imap 'error' 事件处理
		fail ('IMAP 连接出错, 原因: ' + msg.errSrc + ', 详情: ' +
			msg.errStr + ', 状态: ' + msg.state);
		if (msg.errSrc != 'authentication')
		    return;
		setMailFetchInterval(0); // 清除 imapConnectIntervalID
		fail ('邮箱登录失败，未通过服务器认证, 可能口令或授权码' +
			'已过期或失效。请更改邮箱设置, 确保邮箱是有效的, ' +
			'然后重启本软件。');
		return;
	    }

	    if (msg.event == 'end') {	// imap 'end' 事件处理
		fail ('IMAP 连接终止。' + msg.state);
		return;
	    }

	    if (msg.event == 'close') {	// imap 'close' 事件处理
		fail ('IMAP 连接关闭，' + (msg.err ? '有' : '没有') +
			'错误发生。' + msg.state);
		return;
	    }

	    if (msg.event == 'alert') {	// imap 'alert' 事件处理
		fail ('IMAP 发出警告: ' + msg.err + ' ' + msg.state);
		return;
	    }

	    if (msg.event == 'exit') {	// 子进程退出
		fail ('recvmail 子进程即将退出。' + msg.state);
		return;
	    }

	    if (msg.event == 'info') {	// 显示信息
		fail (msg.txt + '&nbsp;' + msg.state);
		return;
	    }

	    if (msg.event == 'no_mail') { // 没有新邮件, 调整邮件检查频率
		adjustMailFetchInterval ();
		return;
	    }

	    if (msg.event == 'uid') {	// 记录 uid
		if (! stored_mailStartUID ||
		      stored_mailStartUID < msg.uid + 1) {
		    stored_mailStartUID = msg.uid + 1;
		    miaoconfig.mailStartUID = msg.uid + 1;
		    miaoconfig_needupdate = true;
		    update_miaoconfig();
		    let uid = window.document.getElementById('mailStartUID');
		    uid.value = stored_mailStartUID;
		}
		return;
	    }

	    if (msg.event == 'parse') {	// 解析邮件, 把结果显示在聊天窗中
		if (! fs.existsSync(msg.eml)) {
		    fail ('错误: 不存在文件 ' + msg.eml);
		    return;
		}
		let rs = fs.createReadStream(msg.eml);
		rs.on('error', (err) => {
		    fail ('读取流出错！' + err.stack);
		    if (! rs.destroyed)
			rs.destroy();
		});
	
		rs.on('close', function() {
		    //fail ('读取流已关闭');
		    if (! rs.destroyed)
			rs.destroy();
		});

		rs.on('end', function() {
		    //fail ('读取流已结束');
		    if (! rs.destroyed)
			rs.destroy();
		});

		let Rs = rs;
		if (msg.bad_mail_charset) {
		    if (! iconv) {
			fail ('处理邮件时出错: iconv 模块未加载。现在加载...');
			iconv = require('./node_modules/iconv-lite');
			if (iconv) {
			    fail ('模块 iconv 成功加载, 继续处理邮件。');
			} else {
			    fail ('模块 iconv 加载失败, 无法完整处理此邮件!');
			}
		    }
		    if (iconv)
			Rs = rs.pipe(iconv.decodeStream(msg.bad_mail_charset));
		}

		const rl = readline.createInterface({
			input: Rs,
			crlfDelay: Infinity
		});

		let bad_mail = false;
		let bad_mail_charset = '';
		let header = {};
		let nonEmptyLines = 0;
		let boundary_array = [];
		let boundary;
		let last_var = 'uncaring';
		let all_done;
		let do_text = null;
		let do_html = null;
		let text = '';
		let html = '';
		let encoding = '';
		let textEncoding = '';
		let htmlEncoding = '';
		let charset = '';
		let textCharset = '';
		let htmlCharset = '';
		let ws = null;
		let files = '';
		let file = '';
		let encoded_file = '';
		let lineno = 0;
		rl.on('line', (line) => {
		    lineno ++;
		    // 若 line 含有 U+FFFD, 则表示解码失败
		    if (! bad_mail && /\ufffd/.test(line)) {
			bad_mail = true;
		    }
		    if (all_done) {  // 停止处理, 退出
			rl.close();
			return;
		    }
		    if (do_text) { // 处理正文
			if (boundary && (
				line == '--' + boundary || 
				line == '--' + boundary + '--'
				)) { //正文边界
			    header.contentID = '';
			    header.contentType = '';
			    header.contentEncoding = '';
			    header.contentDisposition = '';
			    encoding = '';
			    do_text = null;
			    do_html = null;
			    ws = null;
			    nonEmptyLines = 0;
			    if (line == '--' + boundary + '--') { //尾边界
				// 恢复先前的 boundary
				boundary = boundary_array.pop();
				if (! boundary) {
				// 先前没有 boundary，说明已至尾部
				// 结束邮件处理
				all_done = true;
				rl.close();
				}
			    }
			    return;
			}
			textEncoding = encoding;
			textCharset = charset;
			if (encoding == 'base64') {
			    text += line.replace(/^\s+/, '')
				.replace(/\s+$/, '');
			    nonEmptyLines ++;
			    return;
			}
			if (encoding == 'quoted-printable') {
			    text += line.replace(/^\s+/, '')
				.replace(/=\s*$/, '');
			    nonEmptyLines ++;
			    return;
			}
			text += line;
			nonEmptyLines ++;
			return;
		    }
		    if (do_html) { // 处理html
			if (boundary && (
				line == '--' + boundary || 
				line == '--' + boundary + '--'
				)) { //html边界
			    header.contentID = '';
			    header.contentType = '';
			    header.contentEncoding = '';
			    header.contentDisposition = '';
			    encoding = '';
			    do_text = null;
			    do_html = null;
			    ws = null;
			    nonEmptyLines = 0;
			    if (line == '--' + boundary + '--') { //尾边界
				// 恢复先前的 boundary
				boundary = boundary_array.pop();
				if (! boundary) {
				// 先前没有 boundary，说明已至尾部
				// 结束邮件处理
				all_done = true;
				rl.close();
				}
			    }
			    return;
			}
			htmlEncoding = encoding;
			htmlCharset = charset;
			if (encoding == 'base64') {
			    html += line.replace(/^\s+/, '')
				.replace(/\s+$/, '');
			    nonEmptyLines ++;
			    return;
			}
			if (encoding == 'quoted-printable') {
			    html += line.replace(/^\s+/, '')
				.replace(/=\s*$/, '');
			    nonEmptyLines ++;
			    return;
			}
			html += line;
			nonEmptyLines ++;
			return;
		    }
		    if (ws) {	// 处理附件
			if (boundary && (
				line == '--' + boundary || 
				line == '--' + boundary + '--'
				)) { //附件边界
			    let cid = header.contentID;
			    if (/^<[A-Za-z0-9]+\.[A-Za-z0-9]*>$/.test(cid))
				cid = cid.slice(1, -1);
			    else
				cid = '';
			    //文件名:传输编码:cid:charset:实际编码的文件名
			    //注意实际编码的文件名有可能包含 ':'(冒号)
			    files += file + ':' + encoding + ':' + cid +
				':' + charset + ':' + encoded_file +
				'\0';
			    ws.end();
			    header.contentID = '';
			    header.contentType = '';
			    header.contentEncoding = '';
			    header.contentDisposition = '';
			    encoding = '';
			    do_text = null;
			    do_html = null;
			    ws = null;
			    nonEmptyLines = 0;
			    if (line == '--' + boundary + '--') { //尾边界
				// 恢复先前的 boundary
				boundary = boundary_array.pop();
				if (! boundary) {
				// 先前没有 boundary，说明已至尾部
				// 结束邮件处理
				all_done = true;
				rl.close();
				}
			    }
			    return;
			}
			if (encoding == 'base64') { // 暂不支持其它编码
			    ws.write(Buffer.from(line.replace(/^\s+/, '')
				.replace(/\s+$/, ''), 'base64'));
			}
			nonEmptyLines ++;
			return;
		    }
		    if (/^Date:/i.test(line)) {
			header.date = line.slice(5)
				.replace(/^\s+/, '')
				.replace(/\s+$/, '');
			last_var = 'date';
			nonEmptyLines ++;
			return;
		    }
		    if (/^From:/i.test(line)) {
			header.from = line.slice(5)
				.replace(/^\s+/, '')
				.replace(/\s+$/, '');
			last_var = 'from';
			nonEmptyLines ++;
			return;
		    }
		    if (/^To:/i.test(line)) {
			header.to = line.slice(3)
				.replace(/^\s+/, '')
				.replace(/\s+$/, '');
			last_var = 'to';
			nonEmptyLines ++;
			return;
		    }
		    if (/^Subject:/i.test(line)) {
			header.subject = line.slice(8)
				.replace(/^\s+/, '')
				.replace(/\s+$/, '');
			last_var = 'subject';
			nonEmptyLines ++;
			return;
		    }
		    if (/^Content-Type:/i.test(line)) {
			header.contentType = line.slice(13)
				.replace(/^\s+/, '')
				.replace(/\s+$/, '');
			last_var = 'contentType';
			nonEmptyLines ++;
			return;
		    }
		    if (/^Content-ID:/i.test(line)) {
			header.contentID = line.slice(11)
				.replace(/^\s+/, '')
				.replace(/\s+$/, '');
			last_var = 'contentID';
			nonEmptyLines ++;
			return;
		    }
		    if (/^Content-Transfer-Encoding:/i.test(line)) {
			header.contentEncoding = line.slice(26)
				.replace(/^\s+/, '')
				.replace(/\s+$/, '');
			encoding = header.contentEncoding.toLowerCase();
			last_var = 'contentEncoding';
			nonEmptyLines ++;
			return;
		    }
		    if (/^Content-Disposition:/i.test(line)) {
			header.contentDisposition = line.slice(20)
				.replace(/^\s+/, '')
				.replace(/\s+$/, '');
			last_var = 'contentDisposition';
			nonEmptyLines ++;
			return;
		    }
		    if (/^\s+\S/.test(line)) { // 接续前一行
			if (last_var != 'uncaring')
			    header[last_var] += line
				.replace(/^\s+/, '')
				.replace(/\s+$/, '');
			nonEmptyLines ++;
			return;
		    }
		    if (line == '--' + boundary) { // 正文或附件开始
			header.contentID = '';
			header.contentType = '';
			header.contentEncoding = '';
			header.contentDisposition = '';
			encoding = '';
			do_text = null;
			do_html = null;
			ws = null;
			nonEmptyLines = 0;
			return;
		    }
		    if (line == '--' + boundary + '--') { //正文或附件结尾
			header.contentID = '';
			header.contentType = '';
			header.contentEncoding = '';
			header.contentDisposition = '';
			encoding = '';
			do_text = null;
			do_html = null;
			ws = null;
			nonEmptyLines = 0;
			// 恢复先前的 boundary
			boundary = boundary_array.pop();
			if (! boundary) {
			    // 先前没有 boundary，说明已至尾部
			    // 结束邮件处理
			    all_done = true;
			    rl.close();
			}
			return;
		    }
		    if (! /^$/.test(line)) { // 其它非空行，不关心
			last_var = 'uncaring';
			nonEmptyLines ++;
			return;
		    }
		    // 遇到空行, 这表示 header 结束, 正文或附件开始
		    if (! header.contentType) {
			if (! nonEmptyLines)
			    return; // 结束处理此空行
			// 在一些非空行之后遇到此空行，但却不存在
			// Content-Type 行，这视为邮件格式错误。
			fail ('邮件格式错误: 遇到空行但未出现 ' +
			     'Content-Type 行。这条消息的处理' +
			     '在此中断。消息号: ' + seqno +
			     ', 行号: ' + lineno + ', 日期: ' +
			     header.date + ', 发件: ' +
			     JSON.stringify(header.from)
				.replace(/</g, '&lt;')
				.replace(/>/g, '&gt;') +
			     ', 收件: ' +
			     JSON.stringify(header.to)
				.replace(/</g, '&lt;')
				.replace(/>/g, '&gt;'));
			// 结束邮件处理
			all_done = true;
			rl.close();
			return; // 结束处理此空行
		    }
		    // 此行是空行, 但此前已有 Content-Type 行
		    // 从 Content-Type 中抽取 charset
		    if (/;\s*charset=/i.test(header.contentType)) {
			charset = header.contentType
			    .replace(/^.*;\s*charset=/i, '')
			    .replace(/;.*$/, '')
			    .replace(/\x22/g, '') // 去除双引号
			    .toLowerCase();
			if (! bad_mail_charset)
			    bad_mail_charset = charset;
		    }
		    // 从 Content-Type 中抽取 boundary
		    if (/^multipart\/.*;\s*boundary=.*$/i
			    .test(header.contentType)) {
			//如果先前已有 boundary, 把它推入数组保存起来
			if (boundary)
			    boundary_array.push (boundary);
			boundary = header.contentType
			    .replace(/^multipart\/.*;\s*boundary=/i, '')
			    .replace(/;.*$/,'')
			    .replace(/\x22/g, ''); // 去除双引号
			return; // 结束处理此空行
		    }
		    // 如果 Content-Type 指示 text, 则准备读取正文
		    if (/^text\/plain/i.test(header.contentType)) {
			do_text = true;
			return; // 结束处理此空行
		    }

		    if (/^text\/html/i.test(header.contentType)) {
			do_html = true;
			return; // 结束处理此空行
		    }

		    if (/^multipart\/\w+/i
			    .test(header.contentType)) {
			return; // 结束处理此空行
		    }
		    // 处理附件 (如 application/octet-stream, image 等)
		    if (! /^attachment\b/i.test(header.contentDisposition)
			&& ! /^application\/\w+/i.test(header.contentType)
			&& ! /^image\/\w+/i.test(header.contentType)) {
			// Content-Type 的格式未知，视为邮件格式错误。
			fail ('邮件格式错误: 遇到未知格式的 ' +
			     'Content-Type 行。这条消息的处理' +
			     '在此中断。' + ' Content-Type: ' +
			     JSON.stringify(header.contentType));
			// 结束邮件处理
			all_done = true;
			rl.close();
			return; // 结束处理此空行
		    }
		    // 解析 body, 准备写文件
		    if (encoding != 'base64') { // 暂不支持其它编码
			fail ('警告! 附件不是 base64 格式, 内容置空!');
		    }
		    file = '';
		    let c = header.contentType;
		    let i = c.indexOf('name=');
		    if (i >= 0)
			file = c.slice(i + 5).replace(/;.*$/,'');
		    c = header.contentDisposition;
		    if (! file && c) {
			i = c.indexOf('filename=');
			if (i >= 0)
			    file = c.slice(i + 9).replace(/;.*$/,'');
		    }
		    if (! file) {
			fail ('出错: 附件没有文件名。Content-Type: ' +
				header.contentType +
				'<br/>Content-Disposition: ' +
				header.contentDisposition);
			//all_done = true;
			//rl.close();
			//return;
		    }

		    encoded_file = file;
		    let d = path.resolve('../shared/mailbox');
		    file = '../shared/mailbox/tmp_name';

		    if (! mkdir_recursive(d)) {
			fail ('创建文件夹 ' + d + ' 失败! ');
			rl.close();
			return;
		    }
		    // 若文件已存在, 创建临时文件
		    label_tmpfile:
		    if (fs.existsSync(file)) {
			let h;
			try {
				h = fs.mkdtempSync(file + '.');
			} catch (e) {
				fail ('创建临时文件夹 ' + file +
				     '.?????? 失败。' + e);
				file = null;
				break label_tmpfile;
			}
			// 先删除文件夹 h, 然后才能创建文件 h
			try {
				fs.rmdirSync(h);
			} catch (e) {
				fail ('删除文件夹 ' + h + ' 失败。' + e);
				file = null;
				break label_tmpfile;
			}
			file = h;
		    }
		    if (! file) {
			rl.close();
			return;
		    }
		    ws = fs.createWriteStream(file);
		    ws.on('error', (err) => {
			fail ('写文件 ' + file + ' 失败。' + err);
			rl.close();
			ws.end();
			ws = null;
		    });
		    ws.on('close', () => {
		    });
		    return;
		});

		rl.on('close', () => {
		    if (! bad_mail) {
			recvMail_on_message ({
				event:	'fetched',
				date:	 header.date,
				from:	 header.from,
				to:	 header.to,
				subject: header.subject,
				textEncoding: textEncoding,
				textCharset: textCharset,
				text:	 text,
				html:	 html,
				htmlEncoding: htmlEncoding,
				htmlCharset: htmlCharset,
				files:	 files
			});
			return;
		    }
		    if (msg.bad_mail_charset) {
			fail ('原始邮件编码有错, 尝试用 ' +
				msg.bad_mail_charset + ' 解码时失败。'
				+ msg.eml);
			return;
		    }
		    if (! bad_mail_charset) {
			fail ('原始邮件编码有错, 无法解码。' + msg.eml);
			return;
		    }
		    // 用 bad_mail_charset 解码原始邮件, 再试一次
		    msg.bad_mail_charset = bad_mail_charset;
		    recvMail_on_message (msg);
		});

		return;
	    }

	    if (msg.event == 'fetched') { // 收到邮件
		if (! iconv) {
		    fail ('处理邮件时出错: iconv 模块未加载。现在加载...');
		    iconv = require('./node_modules/iconv-lite');
		    if (iconv) {
			fail ('模块 iconv 成功加载, 继续处理邮件。');
		    } else {
			fail ('模块 iconv 加载失败, 无法处理此邮件!');
			return;
		    }
		}
		let senderMail = msg.from
			.replace(/^.*(\b[\w.-]+\b@\b[\w.-]+\b)[^@\w.-]*$/,
				'$1')
			.replace(/[\s\x22]/g, ''); // 去除空白、双引号
		let r = /\s*=\?([A-Za-z0-9-]+)\?([BbQq])\?([^\?]*)\?=/g;
		let err;
		let senderName;
		if (senderMail == msg.from.replace(/[\s\x22]/g, ''))
		    senderName = senderMail;
		else
		    senderName = msg.from
			.replace(/\s*((<.*@.*>)|(\".*@.*\"))/, '')
			.replace(r, function (match, p1, p2, p3) {
			    if (/^[Bb]$/.test(p2)) {
				//return d.decode(Buffer.from(p3, 'base64'));
				return iconv.decode(
				    Buffer.from(p3, 'base64'),
				    p1.toLowerCase());
			    }
			    // quoted-printable
			    return iconv.decode(Buffer.from(
				p3.replace(/=[A-Fa-f0-9]{2}/g, function(match){
					return String.fromCharCode(parseInt
						(match.slice(1,3),16));
				}), 'binary'), p1.toLowerCase());
			})
			.replace(/\x22/g, '') // 去除双引号
			.replace(/[\s\0-,/:-@[-`{|}\x7f]/g, '_');
		if (err) {
		    fail ('解析发送者名字时出错。' + err);
		    senderName = '解析发送者名字时出错';
		    err = null;
		}
		// 若 senderName 含有 U+FFFD, 则表示解码失败
		if (/\ufffd/.test(senderName))
		    senderName = '发信者名字解码出错（信件不遵守邮件规范）';
		if (/^\s*$/.test(senderName)) // 空白
		    senderName = senderMail;
		let subject = msg.subject
			.replace(r, function (match, p1, p2, p3) {
			    if (/^[Bb]$/.test(p2)) {
				//return d.decode(Buffer.from(p3, 'base64'));
				return iconv.decode(
				    Buffer.from(p3, 'base64'),
				    p1.toLowerCase());
			    }
			    // quoted-printable
			    return iconv.decode(Buffer.from(
				p3.replace(/=[A-Fa-f0-9]{2}/g, function(match){
					return String.fromCharCode(parseInt
						(match.slice(1,3),16));
				}), 'binary'), p1.toLowerCase());
			})
			.replace(/\x22/g, ''); // 去除双引号
		if (err) {
		    fail ('解析主题出错。' + err);
		    subject = 'error';
		    err = null;
		}
		let subj = subject.split(':');
		let rand;
		let nick;
		let wgrp;
		let isMiao = 0;
		if (subj.length >= 8 && /^[A-Fa-f0-9]{16}$/.test(subj[0]))
		    isMiao = -1; // 对方知道咱是 miao 用户，已正常互通
		let Id;
		if (isMiao) {
		    //if (! subj[4] || ! subj[5])
			Id = subj[0];
		    //else
			//Id = subj[0] + '@' + subj[4] + ':' + subj[5];
		    rand = subj[0];
		    nick = subj[2];
		    wgrp = subj[3];
		} else if (/[A-Fa-f0-9]{16} \w+ /.test(senderName)) {
		    // 对方向咱表明其 miao 用户身份，但不知道咱也是 miao 用户
		    isMiao = -2;
		    let n = senderName.split(' ');
		    Id = n[0];
		    rand = n[0]
		    nick = n[1];
		    wgrp = n[2];
		} else for (let id in Pal) {
		    if ( // Pal[id].haskey &&
			Pal[id].version.split('#')[8] == senderMail
				.replace(/@ethereal\.email$/, '')) {
			Id = id;
			break;
		    }
		}
		if (isMiao) {
		    let friends = stored_friends.split(/[\r\n]+/);
		    let found = 0;
		    for (let i in friends) {
			if (friends[i].replace(/[\s]+/g, '') == '')
			    continue; // 跳过空白行
			let v = friends[i].split(/\s+/);
			if (senderMail == v[0]) {
			    found |= 1;
			    if (rand == v[1]) {
				if (nick == v[2] && wgrp == v[3]) {
				    found |= 4; // 无需更新好友设置
				} else {
				    friends[i] = senderMail + ' ' + rand
					+ ' ' + nick + ' ' + wgrp;
				    found |= 2; // 需要更新好友设置
				}
			    }
			    else if (/^0+$/.test(v[1])) {
				friends[i] = senderMail + ' ' + rand
					+ ' ' + nick + ' ' + wgrp;
				found |= 2; // 需要更新好友设置
			    }
			}
		    }
		    if (found == 1) {
			friends.push(senderMail + ' ' + rand
					+ ' ' + nick + ' ' + wgrp
			);
			found |= 2; // 需要更新好友设置
		    }
		    if (found == 3) { // 更新好友设置
			let tmp = window.document.getElementById('friends');
			tmp.value = friends.join('\n');
			tmp.onchange();
		    }
		}
		if (! Id)
		    Id = senderMail;  // 添加新用户
		let Msg = {
			version:	'1_lbt6_0#131200#' +
					(isMiao ? rand : '000000000000') +
					'#0#0#0#4001#9#' + senderMail
					.replace(/@ethereal\.email$/, ''),
			SN:		0,
			id:		Id,
			host:		isMiao == -1 ? subj[7] : '',
			cmd:		IPMSG_SENDMSG,
			mainbody:	isMiao ? '' : (
					'日期: ' + msg.date + '<br/>' +
					'作者: ' + senderName + '<br/>' +
					'主题: ' + subject + '<br/>'),
			files:		''
		};
		if (! Pal[Id]) {
		    Pal[Id] = {};
		}
		Pal[Id].nick = Pal[Id].nick || (isMiao ? nick : senderName);
		Pal[Id].wgroup = Pal[Id].wgroup || (isMiao ? wgrp : 'mail');
		Pal[Id].IP = Pal[Id].IP || '';
		Pal[Id].port = Pal[Id].port || '';
		Pal[Id].login = Pal[Id].login || (isMiao == -1 ? subj[6] : '');
		Pal[Id].host = Pal[Id].host || (isMiao == -1 ? subj[7] : '');
		Pal[Id].charset = Pal[Id].charset || 'utf8';
		Pal[Id].version = Pal[Id].version || Msg.version;
		Pal[Id].chatwin = Pal[Id].chatwin || 0;
		Pal[Id].myIPs = Pal[Id].myIPs || '';
			// 通过邮件获取的 myIPs 不可靠，注释掉。
			// ((isMiao == -1 && subj[8])?
			// subj[8].replace(/-/g, ':') : '');
		Pal[Id].yourIP = Pal[Id].yourIP || '';
		Pal[Id].yourPort = Pal[Id].yourPort || '';
		Pal[Id].services = Pal[Id].services || '';
		Pal[Id].time = Date.parse(msg.date);
		if (isNaN(Pal[Id].time))
		    Pal[Id].time = Date.now();
		Pal[Id].mediacy = Pal[Id].mediacy || '';
		Pal[Id].haskey = Pal[Id].haskey || isMiao;
		Pal[Id].offline = Pal[Id].offline || 1;
		Pal[Id].recv = Pal[Id].recv || 0;
		Pal[Id].send = Pal[Id].send || 0;
		let keyStr = null;
		if (isMiao)
		    keyStr = miaoconfig['publicKey' + subj[0]];
		if (keyStr)
		    Pal[Id].haskey = 1;
		updateTable(Id);

		if (isMiao == -1 && subj[8]) {
		    let ip = subj[8].replace(/-/g, ':').split(',');
		    fail ('成功获取 ' + Pal[Id].nick +
			  '(Id=' + Id + ') 的 IP：' + ip);
		    // 获取 IP 后立即发送探测消息
		    for (let i in ip) {
			setTimeout( () => {
			    //who.IP 会被外部代码改变，因此应在此执行
			    who.IP = '255.255.255.255';
			    who.port = 2425;
			    detectSubnets(ip[i].replace(/\/\d+$/, ''), false);
			}, i * 5000); // 设定探测间隔
		    }
		}
		// 消息到达后, 以邮件发送时间来确定邮件检查频率
		if (last_mail_time < Pal[Id].time)
		    last_mail_time = Pal[Id].time;
		adjustMailFetchInterval ();

		let text;
		let html;
		let charset = msg.textCharset || 'utf-8';
		if (msg.text) {
		    if (msg.textEncoding == 'base64') {
			text = iconv.decode(
			    Buffer.from(msg.text, 'base64'),
			    charset);
		    } else if (msg.textEncoding == 'quoted-printable') {
			text = iconv.decode(Buffer.from(msg.text
				.replace(/=[A-Fa-f0-9]{2}/g, function(match){
					return String.fromCharCode(parseInt
						(match.slice(1,3),16));
				}), 'binary'), charset);
		    } else {
			text = msg.text;
		    }
		    if (! isMiao)
			Msg.mainbody += text;
		}

		charset = msg.htmlCharset || 'utf-8';
		if (msg.html) {
		    if (msg.htmlEncoding == 'base64') {
			html = iconv.decode(
			    Buffer.from(msg.html, 'base64'),
			    charset);
		    } else if (msg.htmlEncoding == 'quoted-printable') {
			html = iconv.decode(Buffer.from(msg.html
				.replace(/=[A-Fa-f0-9]{2}/g, function(match){
					return String.fromCharCode(parseInt
						(match.slice(1,3),16));
				}), 'binary'), charset);
		    } else {
			html = msg.html;
		    }
		    if (! isMiao)
			Msg.mainbody += html;
		}
		if (isMiao)
		    Msg.mainbody = html ? html : text;
		if (isMiao == -1) {
		    if (subj[1] == 'SENDMSG') {	// 对方发来消息
			if (Pal[Id].haskey != 1 && stored_friends
				.split(/\s[^\r\n]*[\r\n]{0,}/)
				.indexOf(senderMail) != -1) {
			    // 顺便向好友索要 key
			    SendMail (Id, 'ASKKEY', subj[0], () => {});
			}
		    }
		}
		if (! msg.files) {
		    if (! isMiao) {
			openChat (Msg);
			return;
		    }
		    if (subj[1] == 'SENDMSG') {	// 对方发来消息
			openChat (Msg);
			return;
		    }
		    if (subj[1] == 'ASKKEY') { // 对方索要 key
			// 检查对方是否为好友
			if (stored_friends.split(/\s[^\r\n]*[\r\n]{0,}/)
			    .indexOf(senderMail) == -1)
				return; // 不是好友，忽略请求
			// 如果咱没有好友的 key，也向好友索要
			if (Pal[Id].haskey != 1) {
			    SendMail (Id, 'ASKKEY', subj[0], () => {});
			}
			// 对方只是请求获取 IP 地址
			if (Msg.mainbody.toLowerCase() == 'ip') {
			    // 向对方发送 IP 地址
			    SendMail (Id, 'SENDKEY', 'ip', () => {});
			    return;
			}
			// 检查是不是向咱索要
			if (Msg.mainbody != stored_randomID)
			    return; // 不是向咱索要，忽略请求

			// 检查通过，给好友发送 key
			SendMail (Id, 'SENDKEY', subj[0] +
			    miaoconfig.publicKey, () => {});
			return;
		    }
		    if (subj[1] == 'SENDKEY') { // 对方发来 key
			// 检查对方是否为好友
			if (stored_friends.split(/\s[^\r\n]*[\r\n]{0,}/)
			    .indexOf(senderMail) == -1)
				return; // 不是好友，忽略发来的 key
			// 对方只是发送 IP 地址
			if (Msg.mainbody.toLowerCase() == 'ip')
			    return; // 前面已经探测了返回的 IP，故直接退出

			// 检查是不是发给咱
			if (Msg.mainbody.slice(0,16) != stored_randomID)
				return; // 不是发给咱，忽略发来的 key

			// 检查通过，接受好友发来的 key

			let keyStr = miaoconfig['publicKey' + subj[0]];
			if (! keyStr) {
			    miaoconfig['publicKey' + subj[0]]
				= Msg.mainbody.slice(16);
			    miaoconfig_needupdate = true;
			    update_miaoconfig();
			}
			Pal[Id].haskey = 1;
			return;
		    }
		    fail ('邮件标题包含未知命令: ' + subj[1]);
		    return;
		}
		let files = msg.files.split('\0');
		let i;
		for (i = 0; i < files.length && files[i]; i++) {
			//文件名:传输编码:cid:charset:实际编码的文件名
			//注意实际编码的文件名有可能包含 ':'(冒号)
		    let f_orig = files[i].replace(/^([^:]*:){4}/, '')
			.replace(r, function (match, p1, p2, p3) {
			    if (/^[Bb]$/.test(p2)) {
				return iconv.decode(
				    Buffer.from(p3, 'base64'),
				    p1.toLowerCase());
			    }
			    // quoted-printable
			    return iconv.decode(Buffer.from(
				p3.replace(/=[A-Fa-f0-9]{2}/g, function(match){
					return String.fromCharCode(parseInt
						(match.slice(1,3),16));
				}), 'binary'), p1.toLowerCase());
			})
			.replace(/\x22/g, '') // 去除双引号
			.replace(/[\s\0-,/:-@[-`{|}\x7f]/g, '_');
					// 替换单引号等特殊符号

		    if (! f_orig)
			f_orig = 'no_name';
		    f_orig = '../shared/mailbox/' + f_orig; // 添加 dir 前缀
		    f_orig = path.normalize(f_orig);
		    let v = files[i].split(':');
		    let f;
		    f = path.normalize(v[0]);

		    let encoding = v[1]; // 暂时只按 base64 处理
		    let cid = v[2] || '';
		    let time;
		    let dir = '';
		    let charset = v[3]; // 暂时只按 utf-8 处理
		    if (/^[A-Za-z0-9]+\.[A-Za-z0-9]*$/.test(cid)) {
			time = parseInt(cid.replace(/\.\w*$/, ''), 16);
			// 更新 f 的修改时间
			try {
			    fs.utimesSync(f, (time&(-2)), (time&(-2)));
			} catch (e) {
			    fail ('改变 ' + f + ' 的 mtime 失败! ' + e);
			}
			dir = cid.replace(/^\w+\./, '');
			if (dir && ! /[^0-9A-Fa-f]/.test(dir))
			    dir = Buffer.from(dir, 'hex').toString();
			if (dir.slice(0, 5) != 'sync/')
			    dir = '';
			if (dir.slice(-1) != '/')
			    dir = '';
			if (dir)
			    dir = path.normalize(dir);
		    }

		    let g;
		    let h;
		    label_move_file:
		    if (true || Pal[Id].haskey) {  // 移动文件到好友专用文件夹
			let palDirName = (Pal[Id].nick == Id) ?
					 Id.replace(/:/g,'_') : (Pal[Id].nick
					 + Id.replace(/@.*$/,'')
					 .replace(/:/g,'_'));
			let d = path.resolve('..', 'shared', palDirName, dir);
			if (! mkdir_recursive(d)) {
			    fail ('创建文件夹 ' + d + ' 失败! ');
			    break label_move_file;
			}
			let g = path.resolve(d, path.basename(f_orig));

			// 若 g 已存在, 先更名为临时文件
			if (fs.existsSync(g)) {
			    // 在用户专用目录创建临时文件
			    let t = path.resolve('..', 'shared', palDirName,
				path.basename(f_orig));
			    try {
				h = fs.mkdtempSync(t + '.'); // 创建临时文件
			    } catch (e) {
				fail('创建 ' + t + '.?????? 失败! ' + e);
				break label_move_file;
			    }
			    // Windows 下需要先删除 h 才能重命名为 h
			    try {
				fs.rmdirSync(h);	// 删除 h
			    } catch (e) {
				fail('删除临时目录 ' + h + ' 失败! ' + e);
				break label_move_file;
			    }
			    // 看 f 和 g 哪个更旧, 让新的拥有 f_orig 的名称
			    let ftime = fs.statSync(f).mtime.getTime();
			    let gtime = fs.statSync(g).mtime.getTime();
			    // 若 f 比 g 旧, 就更名 f 为 h, 并采用 h
			    if (ftime < gtime) {
				try {
				    fs.renameSync(f, h);  // 更名 f 为 h
				} catch (e) {
				    fail('更名 ' +f+ ' 为 ' +h+ ' 失败! ' + e);
				    break label_move_file;
				}
				f = h; // 采用 h
				break label_move_file;
			    }
			    // 否则, 就更名 g 为 h, 然后更名 f 为 g, 并采用 g
			    try {
				fs.renameSync(g, h);  // 更名 g 为 h
			    } catch (e) {
				fail('更名 ' + g + ' 为 ' + h + ' 失败! ' + e);
				break label_move_file;
			    }
			}
			try {
			    fs.renameSync(f, g);  // 更名 f 为 g
			} catch (e) {
			    fail('更名 ' + f + ' 为 ' + g + ' 失败! ' + e);
			    break label_move_file;
			}

			f = g; // 采用 g
		    }
		    Msg.files += f + '\0';
		}
		openChat (Msg);
		return;
	    }

	    fail ('严重错误！recvmail 子进程发来未知消息: ' +
			JSON.stringify(msg));
    } // recvMail_on_message () 函数定义结束 

    // 创建子进程，收邮件

    function create_child_recvMail () {
	if (recvMail != null)
	    return;

	const child_process = require('child_process');
	recvMail = child_process.fork('email.js', [''], {silent: true});

	if (recvMail == null) {
	    fail ('严重错误！创建 recvmail 子进程失败。');
	    return;
	}
	recvMail.stdout.on('data', (data) => {
	    fail ('捕获 recvmail 子进程 stdout 信息: ' + data);
	});
	recvMail.stderr.on('data', (data) => {
	    fail ('捕获 recvmail 子进程 stderr 信息: ' + data);
	});

	recvMail.on('error', (err) => {
	    fail ('recvmail 子进程出错: ' + err);
	});

	recvMail.on('message', recvMail_on_message);

	recvMail.on('exit', (code) => {
	    fail ('recvmail 子进程已退出，退出码=' + code);
	    recvMail = null;
	    if (code == 0)
		imapConnect();
	    else {
		// 出错时, 邮件检查间隔调整为 5 分钟
		setMailFetchInterval(300000);
	    }
	});
    } // create_child_recvMail () 函数定义结束

    // 创建子进程，邮寄

    function create_child_sendMail () {
	if (sendMail != null)
	    return;

	const child_process = require('child_process');
	sendMail = child_process.fork('email.js', [''], {silent: true});

	if (sendMail == null) {
	    fail ('严重错误！创建 sendmail 子进程失败。');
	    return;
	}
	sendMail.stdout.on('data', (data) => {
	    fail ('捕获 sendmail 子进程 stdout 信息: ' + data);
	});
	sendMail.stderr.on('data', (data) => {
	    fail ('捕获 sendmail 子进程 stderr 信息: ' + data);
	});

	sendMail.on('error', (err) => {
	    fail ('sendmail 子进程出错: ' + err);
	});

	sendMail.on('message', (msg) => {
	    if (msg.event == 'exit') {	// 子进程退出
		fail ('sendmail 子进程即将退出。' + msg.state);
		return;
	    }

	    if (msg.event == 'info') {	// 显示信息
		fail (msg.txt + '&nbsp;' + msg.state);
		return;
	    }

	    if (msg.event == 'sent') { // 子进程邮寄结束 
		fail (msg.txt + '&nbsp;' + 'state=' +
			JSON.stringify(msg.state));
		if (typeof SendMail_callback === 'function') {
			// 邮寄失败时 msg.state == {}, 成功时为 null
			// alert(msg.state);
			SendMail_callback(msg.state);
		}
		return;
	    }

	    fail ('严重错误！sendmail 子进程发来未知消息: ' +
			JSON.stringify(msg));
	});

	sendMail.on('exit', (code) => {
	    fail ('sendmail 子进程已退出，退出码=' + code);
	    sendMail = null;
	});
    } // create_child_sendMail () 函数定义结束

//    let go_to_sleep = false;
//    let go_to_sleep_intervalID;

    // 函数定义，连接 IMAP 收件服务器
    function imapConnect () {
//	// nwjs 不支持对用户非活跃空闲状态的检测, 若访问邮箱, 会给
//	// 收件服务器造成负担, 因此屏蔽掉此功能。
//	if (typeof nw === 'object')
//	    return;
//
//	if (go_to_sleep)
//	    return;
//
//	// https://stackoverflow.com/questions/38707987/
//	// how-to-check-when-the-computer-is-idle-in-nodejs
//	const powerMonitor = require('electron').remote.powerMonitor;
//	let idle = powerMonitor.getSystemIdleTime();
//	if (idle > 600) {
//	    go_to_sleep = true;
//
//	    if (go_to_sleep_intervalID != undefined)
//		return;
//	    go_to_sleep_intervalID = setInterval(()=> {
//		idle = powerMonitor.getSystemIdleTime();
//		if (go_to_sleep) {
//		    if (idle < 600) { // 唤醒了
//			go_to_sleep = false;
//
//			if (go_to_sleep_intervalID != undefined) {
//			    clearInterval (go_to_sleep_intervalID);
//			    go_to_sleep_intervalID = undefined;
//			}
//			imapConnect(); // 立即检查收件箱
//		    }
//		}
//	    }, 120000);
//	    return;
//	}

	// 若不存在 email 用户名或不存在口令，不必连接服务器
	if (! effective_mailuser || ! effective_mailpass)
	    return;
	if (! navigator.onLine) // 无网络，不必连接服务器
	    return;

	// return; // 调试时用于临时屏蔽掉 imapConnect 函数

	create_child_recvMail ();	// 如果没有子进程，创建它
	if (recvMail == null) // 创建失败，退出
	    return;

	while (debug_info.childNodes.length > 500) {
	    debug_info.removeChild(debug_info.firstChild);
	}

	let ethereal = /@ethereal\.email$/.test(effective_mailuser);
	// 通知子进程执行连接
	recvMail.send ({
		user: effective_mailuser,
		pass: effective_mailpass,
		smtppass: ethereal ? effective_mailpass : (stored_smtppass ?
			stored_smtppass : effective_mailpass),
		imaphost: ethereal ? 'imap.ethereal.email' :
			(stored_imaphost ? stored_imaphost :
			'imap.' + effective_mailuser.replace(/^.*@/, '')),
		imapport: ethereal ? 993 : (stored_imapport ?
			stored_imapport : 993),
		imapsecure: ethereal ? 1 : (imap_secure.checked ? 1 : 0),
		imaprejectunauth:
			ethereal ? 0 : (imap_rejectunauth.checked ? 1 : 0),
		mailUID: stored_mailStartUID,
		maildebug: maildebug.checked,
		run: 'imapConnect'
	});
    } // imapConnect () 函数定义结束

    function setMailFetchInterval (ms) {
	if (mailFetchInterval == ms)
	    return;
	if (ms < 1000) {
	    mailFetchInterval = 0;
	    if (imapConnectIntervalID != undefined) {
		clearInterval (imapConnectIntervalID);
		imapConnectIntervalID = undefined;
	    }
	    return;
	}
	mailFetchInterval = ms;
	if (imapConnectIntervalID != undefined)
	    clearInterval(imapConnectIntervalID);
	imapConnectIntervalID = setInterval (imapConnect, ms);
	fail ('邮件循环检查间隔调整为 ' + (ms / 60000) + ' 分钟');
    }

    function adjustMailFetchInterval () {
	let span = Date.now() - last_mail_time;
	if (span > 1500000) { // 间隔 25 分钟以上, 检查频率 5 分钟
	    setMailFetchInterval(300000);
	    return;
	}
	if (span >  900000) { // 间隔 15 分钟以上, 检查频率 2 分钟
	    setMailFetchInterval(120000);
	    return;
	}
	if (span >  300000) { // 间隔  5 分钟以上, 检查频率 1 分钟
	    setMailFetchInterval(60000);
	    return;
	}
	// 间隔 5 分钟以内, 检查频率 0.5 分钟
	setMailFetchInterval(30000);
	return;
    }

    // 函数定义，确定报文的字符集编码特征
    // msg 是 Buffer 类型
    function getCodingStyle (msg) {
	// 转换成 utf8 字符串, 不会丢失 ASCII 字符
	let MSG = msg.toString('utf8', 0, 512);
	if (/^HTTP.*\s+200\s+OK\s+/i.test(MSG)) { // HTTP/1.1 200 OK
		return { charset : 'unknown',
			 knownGB : false,
			 isMiao  : false,
			 isHTTPU : true,
			 MSG : MSG };
	}
	if (/^1_[a-z]{3,4}\d+_\d+#\d+#[0-9a-fA-F]{16}#/.test(MSG)) { // miao
		return { charset : 'utf8',
			 knownGB : false,
			 isMiao  : true,
			 isHTTPU : false,
			 MSG : MSG };
	}
	if (/^1_iptux/.test(MSG)) { // 信使
		return { charset : 'utf8',
			 knownGB : false,
			 isMiao  : false,
			 isHTTPU : false,
			 MSG : MSG };
	}
	if (/^1_lbt\d+_\d+#-{0,1}\d+#[0-9a-zA-Z]{12}#/.test(MSG)) { // 飞秋
		return { charset : 'gb18030',
			 knownGB : true,
			 isMiao  : false,
			 isHTTPU : false,
			 MSG : MSG };
	}
	let cmd = MSG.split(':')[4];
	if (/^\d+$/.test(cmd) && (cmd & (IPMSG_CAPUTF8OPT | IPMSG_UTF8OPT)))
		return { charset : 'utf8',
			 knownGB : false,
			 isMiao  : false,
			 isHTTPU : false,
			 MSG : MSG };
	if (/^1@shiyeline/.test(MSG)) { // 内网通
		return { charset : 'gb18030',
			 knownGB : true,
			 isMiao  : false,
			 isHTTPU : false,
			 MSG : MSG };
	}
	if (/^1:/.test(MSG)) { // 旧版飞鸽传书
		return { charset : 'gb18030',
			 knownGB : true,
			 isMiao  : false,
			 isHTTPU : false,
			 MSG : MSG };
	}
	if (/^\d+\.\d+\.\d+:/.test(MSG)) { // 新版飞鸽传书
		return { charset : 'gb18030',
			 knownGB : true,
			 isMiao  : false,
			 isHTTPU : false,
			 MSG : MSG };
	}
	return { charset : 'unknown',
		 knownGB : false,
		 isMiao  : false,
		 isHTTPU : false,
		 MSG : MSG };
	//msgEncode = jschardet.detect (msg);
	//// { encoding: 'UTF-8', confidence: 0.9690625 }
	//msgEncode = msgEncode.encoding;
	//if (msgEncode == 'ascii')
	//	msgEncode = 'utf8';

	////if (msgEncode == 'windows-1252')
	////	msgEncode = 'utf8';

	//if (msgEncode == 'GB2312')
	//	msgEncode = 'gb18030';
    } // getCodingStyle () 函数定义结束

    let crypto;
    try {
	crypto = require('crypto');
    } catch (err) {
	alert('严重错误! node.js 禁用了 crypto 模块!');
    }

    // 函数定义，处理对方收到文件片段后发来的应答消息

    function acknowledge (msg, id) {
	let packetID = msg.readDoubleLE(32);
	let input = SendFile[packetID];
	if (! input // || ! input.value || input.value == ''
		|| ! input.files || ! input.files.length) {
	    fail ('对方 ' + id + ' 收到文件后应答的原始发包序号 ' +
		packetID + ' 不是由咱发出的, 忽略此应答！');
	    return; // 忽略错误的应答
	}

	let count = input.files.length; // 咱发送的文件总数
	let fno = msg.readUInt16LE(40); // 对方收到的文件序号 0 ～(count -1)
	if (fno < 0 || fno >= count) {
	    fail ('对方 ' + id + ' 收到文件后应答的文件序号 ' + fno +
		' 与咱发送的文件总数 ' + count + ' 相矛盾, 忽略此应答!');
	    return; // 忽略错误的应答
	}
	let f = input.files[fno];	// 咱发送的文件
	let f_size = msg.readDoubleLE(48); // 对方收到的文件长度
	if (f_size != f.size) {		// 与咱发送的文件长度不一致?
	    fail ('对方 ' + id + ' 收到文件后应答的文件长度 ' + f_size +
		' 与咱发送的文件长度 ' + f.size + ' 不一致, 忽略此应答!');
	    return; // 忽略错误的应答
	}
	let f_time = msg.readDoubleLE(56); // 对方收到的文件修改时间
	if (f_time != f.lastModified) {	// 与我发送的文件修改时间不一致?
	    fail ('对方 ' + id + ' 应答的文件修改时间 ' + f_time +
		' 与咱发送的文件修改时间 ' + f.lastModified +
		' 不一致, 忽略此应答!');
	    return; // 忽略错误的应答
	}
	let i = msg.readUInt32LE(64); // 监控器号
	let p = msg.readUInt32LE(72); // 片段号
	let len = msg.readUInt16LE(80); // 0 表示对方接收失败, 无法接收

	let sendfile = Pal[id].send[fno];
	if (! len) {
	    sendfile.monitor[i].p = -1; // 对方接收失败, 咱应立即停止发送
	    sendfile.monitor[i].cb ();
	    return;
	}
	if (sendfile.monitor[i].p == null ||
	    sendfile.monitor[i].p < p) {
	    //fail ('收到片段 ( ' + i + ', ' + p + ') 的应答。');
	    sendfile.monitor[i].p = p;
	    sendfile.monitor[i].cb ();
	} else {
	    //fail ('丢弃片段 ( ' + i + ', ' + p + ') 的应答。');
	}
    } // acknowledge (msg, id) 函数定义结束

    // 函数定义，接收二进制数据，即文件数据。

    function recvBinaryData (msg, id) {
	let ip = Pal[id].IP;
	if (/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(ip)) {
	    // 将 IPv4 转为 IPv6 格式
	    ip = ip.split('.');
	    if ((Number(ip[0]) | Number(ip[1]) |
		 Number(ip[2]) | Number(ip[3])) < 256)
		ip = '::ffff:' +
			((ip[0] << 8) | ip[1]).toString(16) + ':' +
			((ip[2] << 8) | ip[3]).toString(16);
	}
	let notify_failure = function () {
	    // 向对方发送失败应答, 让对方不再发送

	    msg.write(stored_randomID, 16); // 把发送者的 ID 改为自己的 ID
	    msg.writeUInt16LE(0, 80);	    // 片段长度为 0 表示失败

	    // 发送应答
	    udp_socket.send (msg.slice(0,128), Pal[id].port, ip, (e) => {
		if (e) {
		    fail ('向发送者反馈无法接收文件的消息时也失败了。' + e);
		}
	    });
	}
	
	let fno = msg.readUInt16LE(40);

	if (typeof (Pal[id].recv) !== 'object') {
	    fail ('咱从未想要接收 ' + id + ' 发来的文件, 拒收此片段。');
	    notify_failure();
	    return;
	}
	const recvfile = Pal[id].recv[fno];
	if (typeof recvfile !== 'object') {
	    fail ('用户 ' + id + ' 发送的文件序号有错, 拒收此片段。');
	    notify_failure();
	    return;
	}
	let packetID = msg.readDoubleLE(32);
	if (packetID != recvfile.packetID) {
	    fail ('用户 ' + id + ' 发送的文件片段的包序号 ' + packetID +
		' 与咱原先要接收的包序号 ' + recvfile.packetID +
		' 不同, 拒收此片段。');
	    notify_failure();
	    return;
	}
	let f_size = msg.readDoubleLE(48);
	if (f_size != recvfile.sz) {
	    fail ('用户 ' + id + ' 发送的文件片段所属文件的长度 ' + f_size +
		' 与咱原先要接收的文件长度 ' + recvfile.sz +
		' 不同, 拒收此片段。');
	    notify_failure();
	    return;
	}

	let f_time = msg.readDoubleLE(56);
	if (Math.floor(f_time / 1000) != recvfile.tm) {
	    fail ('用户 ' + id + ' 发送的文件片段所属文件的修改时间 ' +
			Math.floor(f_time / 1000) +
		' 与咱原先要接收的文件的修改时间 ' + recvfile.tm +
		' 不同, 拒收此片段。');
	    notify_failure();
	    return;
	}

	let fd = recvfile.fd;

	let i = msg.readUInt32LE(64);
	let p = msg.readUInt32LE(72);
	let len = msg.readUInt16LE(80);
	if (len == 0 && msg.length == 160) {
	    // 对方给出了发送失败通知, 咱按彻底失败处理, 直接结束接收
	    recvfile.failure = p;
	    if (recvfile.pieces > p)
		recvfile.pieces = p;
	    if (fd != null) {
		//try {
		//    fs.ftruncateSync(fd, recvfile.failure * PLEN);
		//} catch (e) {
		//    fail ('截断文件时失败: ' + e);
		//}
		try {
		    fs.closeSync(fd);
		} catch (e) {
		    fail ('关闭文件时失败: ' + e);
		}
	    }
	    recvfile.fd = null;
	    if (typeof (recvfile.cb) === 'function') {
		recvfile.cb(); // 执行回调（更改文件名等）
		recvfile.cb = null; // 保证回调函数只执行一次
	    }
	    // notify_failure(); // 本来是对方通知咱说对方无法发送给咱,
				 // 此时就不要再通知对方了, 否则是无限循环
	    return;
	}
	
	if (msg.length != len + 128)
		return;
	// 记录监控器 i 的片段 p 已收到
	if (recvfile.monitor[i] == undefined) {
	    if (i > p) {
		fail ('严重错误！监控器号 ' + i + ' 大于片段号 ' + p +
		    ' , 拒收此片段。');
		return;
	    }
	}
	const hash = crypto.createHash('sha256');
	hash.update(msg.slice(128));
	let digest = hash.digest();
	if (Buffer.compare(msg.slice(96, 128), digest))
		return;
	let is_new = false;
	if (recvfile.monitor[i] == undefined || recvfile.monitor[i] < p) {
	    is_new = true;
	    recvfile.monitor[i] = p;
	    recvfile.pieces++;
	}

	msg.write(stored_randomID, 16); // 把发送者的 ID 改为自己的 ID

	// 发送应答
	udp_socket.send (msg.slice(0,128), Pal[id].port, ip,
	    function (e) {
		if (e) {
		    fail ('写片段 ' + p + ' 前发送应答时失败: ' + e);
		}
	    });

	//if (! is_new) {
	//    //fail ('丢弃旧片段 ' + p);
	//    return;
	//}

	// 将片段写入文件。若旧的片段已经写入，不再重复写入
	if (is_new && fd != null) {
	    try {
		//recvfile.writing++;
		fs.writeSync(fd, msg, 128, len, p * PLEN);
		//recvfile.writing--;
	    } catch (e) {
		fail ('写片段 ' + p + ' 时失败: ' + e);
		if (recvfile.failure == null || recvfile.failure > p)
		    recvfile.failure = p; // 全部处理完毕之后，将在此处截断文件
		// 在下面的第二次应答中，用长度 0 来表示写入失败
		msg.writeUInt16LE(0, 80);
	    }
	}

	// 等待 100 毫秒, 再次发送应答
	setTimeout( () => {
	    udp_socket.send (msg.slice(0,128), Pal[id].port, ip, (e) => {
		if (e) {
		    fail ('写片段 ' + p + ' 后发送应答时失败: ' + e);
		}
	    });
	}, 100);

	// 检查是否已全部接收
	if (recvfile.pieces * PLEN >= f_size) {
	    if (recvfile.failure != null && fd != null) {
		//try {
		//    fs.ftruncateSync(fd, recvfile.failure * PLEN);
		//} catch (e) {
		//    fail ('截断文件时失败: ' + e);
		//}
	    }
	    if (fd != null) {
		try {
		    fs.closeSync(fd);
		} catch (e) {
		    fail ('关闭文件时失败: ' + e);
		}
	    }
	    recvfile.fd = null;
	    fd = null;
	    if (typeof (recvfile.cb) === 'function') {
		recvfile.cb(); // 执行回调（更改文件名等）
		recvfile.cb = null; // 保证回调函数只执行一次
	    }
	}

	return;
    } // recvBinaryData (msg, id) 函数定义结束

    function tcp_connect_listener (c) {
	let ip = c.remoteAddress;
	if (/:/.test(ip)) { // 处理 IPv6 格式的 IPv4 地址
	    if (/^::ffff:[a-fA-F0-9]{1,4}:[a-fA-F0-9]{1,4}$/i.test(ip)) {
		ip = ip.slice(7).split(':');
		ip[0] = parseInt(ip[0], 16);
		ip[1] = parseInt(ip[1], 16);
		ip = (ip[0] >>> 8) + '.' + (ip[0] & 255) + '.' +
		     (ip[1] >>> 8) + '.' + (ip[1] & 255);
	    } else
	    if (/^::ffff:\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/i.test(ip)) {
		ip = ip.slice(7);
	    } //else // 纯粹的 IPv6 地址，保持不变
		// ip = ip;
	}
	const addr = ip;
	const port = c.remotePort;
	fail ('已连接 tcp 客户端 ' + addr + ':' + port);

	c.on ('error', (err) => {
	    fail ('与 ' + addr + ':' + port + ' 的 tcp 连接出错: ' + err);
	    c.destroy();
	});

	c.on ('close', (had_err) => {
	    fail ('已关闭 ' + addr + ':' + port + ' 的 tcp 连接, ' +
		(had_err ? '有错误发生' : '一切正常') + '。');
	    c.destroy();
	});

	c.on ('end', () => {
	    fail ('已结束 ' + addr + ':' + port + ' 的 tcp 连接。');
	    c.end();
	});

//	const CHUNK_SIZE = 0x40000; // 每次发送数据的最大长度
	let intervalID;
	let input;
	let fileNo;
	let f;
	let f_size;
//	let result;
//	let new_result;
//	let jammed; // 当 c.write() 返回 false 时，下次写入需要等待 drain 事件
	let offset;
	let pos;
//	let reader;

//	c.on ('drain', () => {
//	    fail ('到 ' + addr + ' 的 tcp 连接, 写入缓冲区排空了: ' + jammed +
//		', ' + typeof new_result + ', ' + reader.result);
//	    if (! jammed) return;
//	    if (! input || ! f_size) return;
//	    //if (! new_result) return;
//	    jammed = false;
//	    //result = new_result;
//	    if (! reader.result)
//	        return;
//	    result = Buffer.from(reader.result);
//	    write_result();
//
////	    if (! write_result())
////		jammed = true;
////	    input.nextElementSibling.rows[fileNo].cells[0].innerHTML =
////		(parseInt(c.bytesWritten / f_size * 100000) / 1000) + '%';
//	});

//	function write_result (evt) {
//		if (! c.write(result, (err) => {
//		    if (err) {
//			fail ('写往 ' + addr + ' 时出错了: ' + err);
//			c.end();
//			return;
//		    }
//		    if (c.bytesWritten >= f_size) {
//			input.nextElementSibling.rows[fileNo]
//				.cells[0].innerHTML = '100%';
//			if (intervalID) {
//			    clearInterval(intervalID);
//			    intervalID = null;
//			}
//			c.end ();
//			return;
//		    }
//		}))
//		    jammed = true;
//		offset = pos;
//		pos = f_size;
//		if (pos > offset + CHUNK_SIZE)
//		    pos = offset + CHUNK_SIZE;
//		if (offset < pos)
//		    reader.readAsArrayBuffer (f.slice(offset, pos));
//	}


	c.on ('data', (data) => {
	    fail ('收到 ' + addr + ':' + port + ' 的 tcp 数据 ' +
			JSON.stringify(data.toString()));
//	    let MSG;
//	    const coding = getCodingStyle (data);
//	    if (coding.charset == 'unknown') {
//		c.end ();
//		return;
//	    }
//	    if (coding.charset == 'utf8') {
//		MSG = data.toString ('utf8');
//	    } else {
//		MSG = iconv.decode (data, coding.charset);
//	    }
//	    const items = MSG.split(':');
	    const items = data.toString('utf8').split(':');
	    if (isNaN(items[1]) || isNaN(items[4]) || (items[4] & 255) != 96) {
		fail ('对方请求 tcp 传送文件, 但数据包格式有错! items[1]='
			+ items[1] + ', items[4]=' + items[4]);
		c.end ();
		return;
	    }

	    let packetID = parseInt(items[5], 16);
	    if (isNaN(packetID)) {
		fail ('对方请求 tcp 传送文件, 但数据包格式有错! items[5]='
			+ items[5] + ', 包序号应为十六进制格式。');
		c.end ();
		return;
	    }

	    // 猜测昵称
	    let nick = '';
	    let v = items[0].split('#');
	    if (v.length > 7 && v[0].slice(0,5) == '1_lbt') {
		for (let id in Pal) {
		    if (Pal[id].IP == addr &&
			Pal[id].version.split('#')[2] == v[2]) {
			nick = Pal[id].nick + '(' + id + ')';
			break;
		    }
		}
	    }

	    function queryLAN () {
		if (checkCIDR(addr)) { // 来自内网
		    c.end ();
		    return;
		}
		// 对方来自外网, 查询所有局域网好友
		fail('对方 ' + nick + addr + ':' + port +
			'可能是请求别的主机（而不是本机）发送文件!');
		c.end ();
		return;
//		let IDs = Object.keys(Pal);
//		function send_data (i) {
//		    let id = IDs[i];
//		    if (Pal[id].IP && Pal[id].port &&
//			checkCIDR(Pal[id].IP) &&
//			Pal[id].version.split('#')[2] != stored_randomID) {
//			//nick = Pal[id].nick + '(' + id + ')';
//			//建立连接, 发送data给IP
//			//break;
//	let tcp_client = require('net').Socket();
//	let remoteAddr;
//	let remotePort;
//
//	tcp_client.setTimeout(30000, () => {
//	    tcp_client.end ();
//	    if (i < IDs.length) {
//		c.end ();
//	    }
//	});
//
//	tcp_client.on ('error', (err) => {
//	    fail('与 tcp 服务器 ' + remoteAddr + ':' + remotePort +
//		' 的连接出错: ' + err);
//	    tcp_client.end ();
//	    if (i < IDs.length) {
//		c.end ();
//	    }
//	});
//
//	tcp_client.on ('close', (had_err) => {
//	    fail('已关闭 ' +remoteAddr+ ':' +remotePort+ ' 的 tcp 连接, '
//		+ (had_err ? '有错误发生' : '一切正常'));
//	    if (i < IDs.length) {
//		c.end ();
//	    }
//	});
//
//	tcp_client.on ('end', () => {
//	    fail('已断开 ' +remoteAddr+ ':' +remotePort+ ' 的 tcp 连接');
//	    if (i < IDs.length) {
//		c.end ();
//	    }
//	});
//
//	let dataSize = 0;
//	tcp_client.on ('data', (data) => {
//	    fail ('收到 ' + remoteAddr + ':' + remotePort + ' 的 tcp 数据 ' +
//			JSON.stringify(data.toString()));
//	    if (data.length == 0) {
//		fail ('警告！不该收到来自 ' + remoteAddr + ':' +
//			remotePort + ' 的长度为 0 的 tcp （空）数据包!');
//		return;
//	    }
//	    dataSize += data.length;
//	});
//
//	// 建立连接, 从 s_size 开始接收文件 (断点续传)
//	if (GATEWAYs && GATEWAYs.split(',').indexOf(Pal[id].IP) >= 0) {
//	    fail('对方 ' + Pal[id].nick + '(' + id +
//		') 的飞鸽使用了经过网关周转的 IP 地址 ' + Pal[id].IP + ':' +
//		Pal[id].port + ' 来发送文件, 但这通常会失败!');
//	}
//	// 若端口不是 2425, 或者 IP 是网关的 ip 地址，则 TCP 协议的接收会失败!
//	tcp_client.connect (Pal[id].port, Pal[id].IP, () => {
//	    remoteAddr = tcp_client.remoteAddress;
//	    remotePort = tcp_client.remotePort;
//	    fail('文件请求包转发到 tcp 服务器 ' +remoteAddr+ ':' +remotePort);
//	    tcp_client.write (data);
//	});
//
//		    }
//		    if (i < IDs.length) {
//			send_data(i + 1);
//		    }
//		}
//		send_data(0);
	    }

	    input = SendFile[packetID];

	    if (! input // || ! input.value || input.value == ''
			|| ! input.files || ! input.files.length) {
		fail ('对方请求传送文件, 但包序号与本机不一致！');
		queryLAN();
		//c.end ();
		return;
	    }

	    let count = input.files.length;

	    fileNo = parseInt(items[6], 16) - 1;
	    if (isNaN(fileNo) || fileNo < 0 || fileNo >= count) {
		fail ('对方请求传送文件, 但文件号与本机不一致！');
		queryLAN();
		//c.end ();
		return;
	    }
	    offset = parseInt(items[7], 16);
	    if (isNaN(offset) || offset < 0 ||
				 offset > Number.MAX_SAFE_INTEGER) {
		fail ('对方请求的文件偏移格式有错！');
		queryLAN();
		//c.end ();
		return;
	    }

	    // 从文件的偏移 offset 处开始发送

	    let chatwin = input.ownerDocument.defaultView;
	    f = input.files[fileNo];
	    f_size = f.size;

	    let div_out = input.ownerDocument.getElementById('outputbox');

	    let p = input.ownerDocument.createElement('p');
	    p.innerHTML = '<span style="color:green">' + (new Date())
		.toLocaleString(date_locale, date_options) + ' 向 ' + nick +
		addr + ':' + port + '(tcp) 发送文件 ' + f.path + ', 长度 ' +
		f_size + ' 字节; ' +
		(offset ? ('对方请求断点续传, 跳过头部 ' + offset +
		' 个字节; ') : '') +
		'已发送 </span><span style="color:blue">0%</span>';
	    //p.style.color = 'blue';
	    p.style.margin = 0;
	    //p.style.padding = 0;
	    div_out.appendChild(p);
	    p.scrollIntoView(false);
	    save_history(p.innerHTML, chatwin.pal_id);

	    // nw 和 electron 都支持 f.path 获取真实路径
	    let fileReadStream = fs.createReadStream(f.path, {start: offset});

	    fileReadStream.on('error', (err) => {
		fail ('读本地文件出错！错误信息: ' + err.stack);
		c.end();
		if (! fileReadStream.destroyed)
			fileReadStream.destroy();
	    });
	
	    fileReadStream.on('close', function() {
		fail ('本地文件(读取流)已关闭');
		//c.end();
		if (! fileReadStream.destroyed)
			fileReadStream.destroy();
	    });

	    fileReadStream.on('end', function() {
		fail ('本地文件(读取流)已结束');
		//c.end();
		if (! fileReadStream.destroyed)
			fileReadStream.destroy();
	    });

	    fileReadStream.pipe(c);

//	    reader = new FileReader();
//
//	    reader.onerror = function(event) {
//		reader.abort();
//	    };
//
//	    reader.onload = function (event) {
//		if (jammed) {
////			let tmp_result = Buffer.from(this.result);
////			new_result = Buffer.alloc(tmp_result.length);
////			tmp_result.copy(new_result);
//			// 等待 drain 事件, 由 drain 的事件处理来写本结果
//			return;
//		}
//		result = Buffer.from(this.result);
//		write_result (event);
//	//	if (! write_result (event))
//	//		jammed = true; // 由 drain 事件处理来写下一个结果
////		c.write(Buffer.from(this.result), (err) => {
////		    if (err) {
////			c.end();
////			return;
////		    }
//////		    input.nextElementSibling.rows[fileNo]
//////				.cells[0].innerHTML =
//////			(parseInt(pos / f_size * 100000) / 1000) + '%';
////
////		    offset = pos;
////		    pos = f_size;
////		    if (pos > offset + CHUNK_SIZE)
////			pos = offset + CHUNK_SIZE;
////		    if (offset >= pos) {
////			c.end ();
////			input.nextElementSibling.rows[fileNo]
////				.cells[0].innerHTML = '100%';
////			if (intervalID) {
////			    clearInterval(intervalID);
////			    intervalID = null;
////			}
////			return;
////		    }
////		    reader.readAsArrayBuffer (f.slice(offset, pos));
////		});
//	    };
//	    pos = f_size;
//	    if (pos > offset + CHUNK_SIZE)
//		pos = offset + CHUNK_SIZE;
//	    if (offset >= pos) {
//		c.end ();
//		return;
//	    }
//	    reader.readAsArrayBuffer (f.slice(offset, pos));
//
	    intervalID = setInterval (function () {
		p.firstChild.nextElementSibling.innerHTML =
		//input.nextElementSibling.rows[fileNo].cells[0].innerHTML =
		    (parseInt((c.bytesWritten + offset) / f_size * 100000)
			/ 1000) + '%';
		if (c.bytesWritten >= f_size) {
		    if (intervalID) {
			clearInterval(intervalID);
			intervalID = null;
		    }
		    // 更新已接收人数
		    Pal[chatwin.pal_id].send.input.files[fileNo].recv ++;
		    input.ownerDocument.getElementById('filesToSend')
			.rows[fileNo].cells[0]
			.innerHTML -= -1; // 用加法时会当成字符串合并
		}
	    }, 500);
	});
//	// c.write ('I am from tcp server\r\n');
//	// c.end ();
//	// c.pipe (c);
	c.setTimeout(30000, ()=>{
	    c.end ();
	    if (intervalID) {
		clearInterval(intervalID);
		intervalID = null;
	    }
	});
    }
    //);

    function http_get (location, rinfo, use_rinfo, callback) {
	if (! /^http:\/\//i.test(location)) {
	    if (callback)
		callback (new Error('UPnP 严重错误, 无效的网址: ' + location));
	    return;
	}
	let ip = rinfo.address;
	if (/:/.test(ip)) { // 处理 IPv6 格式的 IPv4 地址
	    if (/^::ffff:[a-fA-F0-9]{1,4}:[a-fA-F0-9]{1,4}$/i.test(ip)) {
		ip = ip.slice(7).split(':');
		ip[0] = parseInt(ip[0], 16);
		ip[1] = parseInt(ip[1], 16);
		ip = (ip[0] >>> 8) + '.' + (ip[0] & 255) + '.' +
		     (ip[1] >>> 8) + '.' + (ip[1] & 255);
	    } else
	    if (/^::ffff:\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/i.test(ip)) {
		ip = ip.slice(7);
	    } else { // 纯粹的 IPv6 地址，退出
		if (callback)
		    callback (new Error('UPnP 错误, 出现 IPv6 地址: ' + ip));
		return;
	    }
	}
	let loc_addr = location.replace (/^http:\/\//i, '');
	let loc_tail = loc_addr.replace (/[^\/]+/, '');
	let loc_host_port = loc_addr.replace (/\/.*$/, '');
	if (/^\[[a-fA-F0-9:]*:[a-fA-F0-9:.]*\]:\d+$/.test(loc_host_port)) {
	    //loc_host_port = loc_host_port.split('\]:');
	    //loc_host_port[0] += '\]';
	    if (callback)
		callback (new Error('UPnP 错误, 出现 IPv6 地址: ' + location));
	    return;
	} else {
	    loc_host_port = loc_host_port.split (':');
	}
	if (loc_host_port.length > 2) {
		if (callback)
		    callback (new Error(
			'UPnP 严重错误: 无效的 host:port 格式: ' +
			loc_host_port));
		return;
	}
	//let rinfo_addr = rinfo.address;
	//if (/:/.test(rinfo_addr))
	//    rinfo_addr = '[' + rinfo_addr + ']';
	if (loc_host_port[0] == '') {
	    location = 'http://' + ip;
	    if (loc_host_port.length == 2)
		location += (':' + loc_host_port[1]);
	    location += loc_tail;
	    loc_host_port[0] = ip;
	}
	if (use_rinfo) {
	    if (loc_host_port[0] == ip) {
		if (! callback)
		    return;
		if (typeof use_rinfo === 'object' && use_rinfo) // err
		    callback (use_rinfo); // use_rinfo 就是上次的错误
		else
		    callback (new Error('重试的 URL 未变化, 不再重试。 '));
		return;
	    }
	    location = 'http://' + ip;
	    if (loc_host_port.length == 2)
		location += (':' + loc_host_port[1]);
	    location += loc_tail;
	}

	const http = require('http');

	http.get(location, (res) => {
	    if (res.statusCode < 200 || res.statusCode > 299) {
		res.resume ();
		if (! use_rinfo)
		    http_get (location, rinfo, true, callback);
		else if (callback)
		    callback (new Error('UPnP 页面请求失败（状态码 ' +
			res.statusCode + '）, 无法获取网页: ' + location));
		return;
	    }
	    let got = '';
	    res.on ('data', (chunk) => { got += chunk; });
	    res.on ('end', () => {
		const parser = new DOMParser();
		const doc = parser.parseFromString (got, 'application/xml');
		// parseFromString 出错时, doc 是含有出错信息的 XMLDocument
		const de = doc.documentElement;
		let urlbase;
		for (let i = 0; i < de.childElementCount; i++) {
		    if (/^URLBase$/i.test(de.children[i].tagName)) {
			urlbase = de.children[i].innerHTML.replace(/\s+/g, '');
			break;
		    }
		}
		if (! urlbase) {
		    urlbase = 'http://' + location
				.replace (/^http:\/\/([^\/]*).*$/i, '$1');
		}
		urlbase = urlbase.replace (/\/+$/, ''); // 去掉结尾的 '/'
		let v = de.getElementsByTagName('service');
		if (! v.length) {
		    fail ('UPnP 出错: root 无 service');
		    return;
		}
		for (let i = 0; i < v.length; i++) {
		    let t = v[i].getElementsByTagName('serviceType');
		    if (! t.length) {
			fail ('UPnP 出错: service 无 serviceType');
			continue;
		    }
		    t = t[0].innerHTML;
		    let ctrl = v[i].getElementsByTagName('controlURL');
		    if (! ctrl.length) {
			fail ('UPnP 出错: service 无 controlURL');
			continue;
		    }
		    ctrl = ctrl[0].innerHTML.replace (/\s+/g, '');
		    let scpd = v[i].getElementsByTagName('SCPDURL');
		    if (! scpd.length) {
			fail ('UPnP 出错: service 无 SCPDURL');
			continue;
		    }
		    scpd = scpd[0].innerHTML.replace (/\s+/g, '');
		    if (! /^http:\/\//i.test(ctrl)) {
			ctrl = urlbase + '/' + ctrl.replace(/^\/+/, '');
		    }
		    if (! /^http:\/\//i.test(scpd)) {
			scpd = urlbase + '/' + scpd.replace(/^\/+/, '');
		    }
		    if (callback)
			callback (null, t, ctrl, scpd);
		}
	    });

	}).on ("error", (err) => {
	    if (! use_rinfo)
		http_get (location, rinfo, err, callback);
	    if (callback)
		callback (err);
	});
    } // http_get () 函数定义结束

    function run_action (loc, t, scpd, action, args, cb) {
	let path = loc.replace (/^http:\/\//i, '').replace (/^[^\/]+/, '');
	let hostPort = loc.replace (/^http:\/\//i, '').replace (/\/.*$/, '');
	let host_port = hostPort.split (':');
	
//	const postData ='<?xml version="1.0" encoding="utf-8"?>\r\n' +
//	    '<soap-env:Envelope soap-env:encodingStyle=' +
//	    '"http://schemas.xmlsoap.org/soap/encoding/" ' +
//	    'xmlns:soap-env="http://schemas.xmlsoap.org/soap/envelope/">\r\n'
//	    + '<soap-env:Body>\r\n' +
//	    '<m:GetExternalIPAddress ' +
//	    'xmlns:m="urn:schemas-upnp-org:service:WANIPConnection:1">\r\n' +
//	    '</m:GetExternalIPAddress>\r\n' +
//	    '</soap-env:Body>\r\n</soap-env:Envelope>';

	const postData =
	    '<?xml version="1.0" encoding="utf-8"?>' +
	    '<s:Envelope xmlns:s=' +
		'"http://schemas.xmlsoap.org/soap/envelope/" ' +
		's:encodingStyle=' +
		'"http://schemas.xmlsoap.org/soap/encoding/">' +
	    '<s:Body><' + 'u:' + action + ' xmlns:u="' + t + '">' +
		args.map((args) => {
		    return '<' + args[0] + '>' +
			(args[1] === undefined ? '' : args[1]) +
			'</' + args[0] + '>';
		}).join('') +
	    '</u:' + action + '>' + '</s:Body>' +
	    '</s:Envelope>';

	const options = {
		hostname:	host_port[0],
		port:		host_port[1] ? host_port[1] : 80,
		path:		path,
		method:		'POST',
		headers:	{
			'SOAPAction': '"' + t + '#' + action + '"',
			'Host': hostPort,
			'Content-Type': 'text/xml; charset="utf-8"',
			'Content-Length': postData.length,
			Connection: 'close',
		},
	};

	const http = require('http');

	const req = http.request(options, (res) => {
	    if (res.statusCode < 200 || res.statusCode > 299) {
		res.resume ();
		if (cb)
		    cb (new Error('Request failed: ' + res.statusCode));
		return;
	    }
	    res.setEncoding ('utf8');
	    let got = '';
	    res.on ('data', (chunk) => {
		got += chunk;
	    });
	    res.on ('end', () => {
	        if (cb)
		    cb (null, got);
	    });
	});

	req.on('error', (err) => {
	    fail ('POST 请求出错: ' + err.message);
	    if (cb)
		cb (err);
	});

	req.write (postData);
	req.end ();
    } // run_action () 函数定义结束

    function do_UPnP (msg, rinfo) {

	let MSG = msg.toString();

	//if (! checkCIDR (rinfo.address))
	//    fail ('收到 ' + rinfo.address + ':' + rinfo.port +
	//		' 发来的 HTTPU 消息: ' + JSON.stringify(MSG)
	//		.replace(/</g, '&lt;').replace(/>/g, '&gt;'));
	// MSG 含多行, 从中获得 rootDesc 文件的位置
	if (! /^LOCATION:/im.test(MSG)) {
	    fail ('UPnP 的如下 M-SEARCH 响应信息不正确:<br/>' +
			JSON.stringify(MSG)
			.replace(/</g, '&lt;').replace(/>/g, '&gt;')
			+ '<br/>无法得到 rootDesc 文件的位置。');
	    if (quit_ongoing > 1) {
		setTimeout(quit_app, 5000, true);
	    }
//	    if (quit_ongoing > 1)
//		quit_ongoing --; // quit_app ();
	    return;
	}

	// 奇怪! 这两种折行的写法通不过（在 WinXP 下）:
	//
	// let location = MSG.replace
	//    (/^(.*[\r\n]+)*LOCATION:\s*(\S*)\s*[\r\n]+[\s\S]{0,}$/i, '$2');
	//
	// 或
	//
	// let location = MSG.replace (
	//    /^(.*[\r\n]+)*LOCATION:\s*(\S*)\s*[\r\n]+[\s\S]{0,}$/i, '$2');
	//
	// 都不行。必须把 .replace 也挪到下一行才行:

	let location = MSG
	    .replace (/^(.*[\r\n]+)*LOCATION:\s*(\S*)\s*[\r\n]+[\s\S]{0,}$/i,
			'$2');

	// 用 http get 获取 rootDesc 文件内容
	http_get (location, rinfo, false, (err, t, ctrl, scpd) => {
	    if (err) {
		if (quit_ongoing > 1) {
		    fail ('UPnP 出错: ' + err + ', URL=' + location);
		    setTimeout(quit_app, 5000, true);
		}
//		if (quit_ongoing > 1)
//		    quit_ongoing --; // quit_app ();
		return;
	    }

	    // 只关注有关 WAN 口操作的服务内容
	    // urn:schemas-upnp-org:service:WANIPConnection:1
	    // urn:schemas-upnp-org:service:WANIPConnection:2
	    // urn:schemas-upnp-org:service:WANPPPConnection:1
	    if (! /^urn:schemas-upnp-org:service:WAN(I|PP)PConnection:\d+$/i
			.test(t)) {
//		if (quit_ongoing > 1)
//		    quit_ongoing --; // quit_app ();
		return;
	    }

	    function getmap (i, port, protocol, callback) {
		protocol && run_action (ctrl, t, scpd,
			'GetSpecificPortMappingEntry', [
				['NewRemoteHost', ''],
				['NewExternalPort', port],
				['NewProtocol', protocol]
			], (err, got) => {
		    if (err) {
			fail ('GetSpecificPortMappingEntry 失败: URL=' +
				ctrl + ', 出错信息: ' + err);
			if (callback)
			    callback (err, false);
			return;
		    }
		    fail ('GetSpecificPortMappingEntry 响应:<br/>' +
			got.replace(/</g, '&lt;').replace(/>/g, '&gt;'));
		    let parser = new DOMParser();
		    let doc = parser.parseFromString (got, 'application/xml');
		    let client = doc.documentElement.getElementsByTagName
				('NewInternalClient')[0];
		    if (! client) {
			fail ('UPnP 获取 ' + protocol + ' 端口 ' + port +
			    ' 的映射信息时, 服务器未给出含有' +
			    ' NewInternalClient 的正确响应。响应如下:<br/>' +
			    got.replace(/</g, '&lt;').replace(/>/g, '&gt;'));
			if (callback)
			    callback (err, false);
			return;
		    }

		    let ip = client.innerHTML.replace (/\s+/g, '');
		    if (! /^\d+\.\d+\.\d+\.\d+$/.test(ip)) {
			fail ('UPnP 获取的 ' + protocol + ' 端口 ' + port +
			    ' 的映射 IP 格式不正确, ip=' + ip +
			    '<br/>服务器的响应:<br/>' +
			    got.replace(/</g, '&lt;').replace(/>/g, '&gt;'));
		    }
		    if (callback)
			callback (err, ip);

		});

		protocol || run_action (ctrl, t, scpd,
			'GetGenericPortMappingEntry', [
				['NewPortMappingIndex', i]
			], (err, got) => {
		    if (err) {
			fail ('GetGenericPortMappingEntry[' + i + '] 失败: '
				+ err + ', URL=' + ctrl);
			if (i === 0)
			    setTimeout (getmap, 20, i + 1);
			return;
		    }
		    fail ('GetGenericPortMappingEntry[' + i + '] 响应: ' +
			got.replace(/</g, '&lt;').replace(/>/g, '&gt;'));
		    setTimeout (getmap, 20, i + 1);
		    return;
		});
	    }
//	    getmap (0);
//	    getmap (0, 2425, 'UDP');
//	    getmap (0, 2425, 'TCP');
//	    return;

	    function delmap (port, protocol, callback) {
		let addr = rinfo.address;
		let myCIDR = checkCIDR (addr);
		let myIP;
		if (myCIDR)
		    myIP = myCIDR.replace(/\/.*$/, '');
		if (! myIP) {
		    addr = ctrl
			.replace(/^http:\/\/([^\/:]+)([\/:].*){0,1}$/i, '$1');
		    myCIDR = checkCIDR (addr);
		    if (myCIDR)
			myIP = myCIDR.replace(/\/.*$/, '');
		}
		if (! myIP) {
		    addr = scpd
			.replace(/^http:\/\/([^\/:]+)([\/:].*){0,1}$/i, '$1');
		    myCIDR = checkCIDR (addr);
		    if (myCIDR)
			myIP = myCIDR.replace(/\/.*$/, '');
		}
		if (! myIP) {
		    fail ('UPnP 出错: 找不到与路由器 ' + rinfo.address +
			'(' + addr + ')' +
			'对应的本机网卡地址, 拒绝删除端口映射。');
		    if (callback)
			callback (new Error('本机IP有错, 拒绝删除端口映射'));
		    return;
		} else if (myIP == addr) {
		    fail ('UPnP 删除端口映射异常警告: 本机IP ' + myIP +
			' 与路由器 ' + rinfo.address + '(' + addr + ')' +
			'的地址相同！继续尝试删除端口映射...');
		}
		run_action (ctrl, t, scpd, 'DeletePortMapping', [
				['NewRemoteHost', ''],
				['NewExternalPort', port],
				['NewProtocol', protocol]
			], (err, got) => {
		    if (err) {
			fail ('删除 ' + protocol + ' 端口 ' + port +
				' 的映射时失败, 映射可能本来就不存在。');
		    } else {
			fail ('成功删除 ' + protocol + ' 端口 ' + port +
				' 的映射。');
		    }
		    if (callback)
			callback (err);
		});
	    }

	    function addmap (port, protocol, callback) {
		let addr = rinfo.address;
		let myCIDR = checkCIDR (addr);
		let myIP;
		if (myCIDR)
		    myIP = myCIDR.replace(/\/.*$/, '');
		if (! myIP) {
		    addr = ctrl
			.replace(/^http:\/\/([^\/:]+)([\/:].*){0,1}$/i, '$1');
		    myCIDR = checkCIDR (addr);
		    if (myCIDR)
			myIP = myCIDR.replace(/\/.*$/, '');
		}
		if (! myIP) {
		    addr = scpd
			.replace(/^http:\/\/([^\/:]+)([\/:].*){0,1}$/i, '$1');
		    myCIDR = checkCIDR (addr);
		    if (myCIDR)
			myIP = myCIDR.replace(/\/.*$/, '');
		}
		if (! myIP) {
		    fail ('UPnP 出错: 找不到与路由器 ' + rinfo.address +
			'(' + addr + ')' +
			'对应的本机网卡地址, 拒绝添加端口映射。');
		    if (callback)
			callback (new Error('本机IP有错, 拒绝添加端口映射'));
		    return;
		} else if (myIP == addr) {
		    fail ('UPnP 添加端口映射异常警告: 本机IP ' + myIP +
			' 与路由器 ' + rinfo.address + '(' + addr + ')' +
			'的地址相同！继续尝试添加端口映射...');
		}
		run_action (ctrl, t, scpd, 'AddPortMapping', [
				['NewRemoteHost', ''],
				['NewExternalPort', port],
				['NewProtocol', protocol],
				['NewInternalPort', port],
				['NewInternalClient', myIP],
				['NewEnabled', 1],
				['NewPortMappingDescription', 'miao'],
				['NewLeaseDuration', 0]
			], (err, got) => {
		    if (err) {
			fail ('添加 ' + protocol + ' 端口 ' + port +
			    ' 到本机 ' + myIP + ' 的映射时失败。注意: 如果' +
			    '路由器的【DMZ 主机】、【虚拟服务器】、' +
			    '【特殊应用程序】、【端口触发】等设置项中已有' +
			    '相应的映射, 则 UPnP 的添加或删除映射操作可能' +
			    '会失败。解决方法: 在路由器的上述设置中去掉所有' +
			    '那些有关 2425 端口的映射。');
		    } else {
			fail ('成功添加 ' + protocol + ' 端口 ' + port +
			    ' 到本机 ' + myIP + ' 的映射。');
		    }
		    if (callback)
			callback (err);
		});
	    }
 
	    if (quit_ongoing > 0) {
		getmap (0, 2425, 'UDP', (err, ip) => {
		    if (ip && ADDRs.split(',').indexOf(ip) != -1)
			delmap (2425, 'UDP');
		    getmap (0, 2425, 'TCP', (err2, ip2) => {
			if (ip2 && ADDRs.split(',').indexOf(ip2) != -1) {
			    delmap (2425, 'TCP', (err3) => {
				quit_app (true);
				//alert('已删除映射, 现在退出。');
				//setTimeout(quit_app, 30000, true);
			    });
			} else {
			    quit_app (true);
			    //alert('没有映射需要删除, 现在退出。');
			    //setTimeout(quit_app, 30000, true);
			}
		    });
		});
		return;
	    }

	    //fail ('正在尝试用 UPnP 获取路由器外网 IP ...');

	    run_action (ctrl, t, scpd, 'GetExternalIPAddress', [],
			(err, got) => {
		if (err) {
		    fail ('UPnP 获取路由器外网 IP 失败, 服务器 URL=' + ctrl +
			', 出错信息: ' + err);
		    return;
		}

		const parser = new DOMParser();
		const doc = parser.parseFromString (got, 'application/xml');
		const WAN = doc.documentElement.getElementsByTagName
				('NewExternalIPAddress')[0];
		if (! WAN) {
		    fail ('UPnP 获取路由器外网 IP 时, 服务器未给出含有' +
			' NewExternalIPAddress 的正确响应。响应如下:<br/>' +
			got.replace(/</g, '&lt;').replace(/>/g, '&gt;'));
		    return;
		}

		let ip = WAN.innerHTML.replace (/\s+/g, '');
		if (! /^\d+\.\d+\.\d+\.\d+$/.test(ip)) {
		    fail ('UPnP 获取的外网 IP 格式不正确, ip=' + ip +
			'<br/>服务器的响应:<br/>' +
			got.replace(/</g, '&lt;').replace(/>/g, '&gt;'));
		    return;
		}

		UPnP_WAN_IP = ip;
		//fail ('UPnP 成功获取路由器外网 IP: ' + ip);

		if (! stored_outer_LAN_IP ||
		    stored_outer_LAN_IP.replace(/\/.*$/, '') != ip) {
		    let outIP = window.document.getElementById('outer_LAN_IP');
		    outIP.value = ip;
		    outIP.onchange ();
		    // 外网 IP 已变, 等待 1 秒后, 刷新一次
		    setTimeout (refresh, 1000);
		}

		// 确定 DMZ 主机是否有响应, 如果无响应, 设定本机为 DMZ

	   	let tmp_time = Date.now(); // 先记录当前时间

		function set_DMZ () {

		    // 向 ip 发探测消息
		    who.IP = ip;
		    sendMsg (Doe, who, IPMSG_BR_ENTRY, Doe.nick);

		    setTimeout(() => {
			for (let id in Pal) {
			    if (Pal[id].IP != ip) // || Pal[id].port != 2425
				continue;
			    if (Pal[id].time < tmp_time)
				continue;
			    //fail ('DMZ 主机 ' + Pal[id].myIPs + ' 响应');
			    return;
			}

			if (Date.now () - tmp_time < 10000) {
			    //fail ('DMZ 主机无响应, 继续等待...');
			    set_DMZ ();
			    return;
			}
			//fail ('DMZ 主机无响应, 尝试设置本机为 DMZ。');
			delmap (2425, 'UDP', (err) => {
			    addmap (2425, 'UDP');
			});
			delmap (2425, 'TCP', (err) => {
			    addmap (2425, 'TCP');
			});
		    }, 200);
		}
		set_DMZ ();
	    });

//	    run_action (ctrl, rinfo, true, t, scpd, 'GetConnectionTypeInfo',
//			[], (err, got) => {
//		if (err) {
//		    fail ('GetConnectionTypeInfo 失败: URL=' + ctrl +
//			', 出错信息: ' + err);
//		    return;
//		}
//		fail ('GetConnectionTypeInfo 响应:<br/>' +
//		    '<textarea readonly>' + got + '</textarea>');
//		return;
//		const parser = new DOMParser();
//		const doc = parser.parseFromString (got, 'application/xml');
//		const newtype = doc.documentElement
//			.getElementsByTagName('NewConnectionType')[0];
//		const possible = doc.documentElement
//			.getElementsByTagName('NewPossibleConnectionTypes')[0];
//		if (newtype) {
//			alert (newtype.innerHTML);
//		}
//		if (possible) {
//			alert (possible.innerHTML);
//		}
//	    });

	});
    }

    function on_quit_app () {

	if (! confirm('您确定要退出 miao 吗？\n注意！退出 miao 时，\n' +
		'所有的聊天窗口都将关闭！'))
	    return false;

	// 如有必要，更新 miaoconfig
	if (miaoconfig_needupdate)
		update_miaoconfig (true);
	// 退出邮箱登录，否则会遗留过多并发连接，使服务器瘫痪
	for (let i = 0; i < 10; i++) {
	    // 执行 10 次，等待子进程退出
	    if (recvMail == null) break;
	    // 通知子进程断开与 imap 服务器的连接
	    recvMail.send ({
		user: effective_mailuser,
		pass: effective_mailpass,
		run: 'imap.end'
	    });
	    // 阻止 imapConnect() 继续启动新的子进程
	    stored_emailuser = null;
	    stored_emailpass = null;
	    stored_mailuser = null;
	    stored_mailpass = null;
	    effective_mailuser = null;
	    effective_mailpass = null;
	}

	//sendMsg (Doe, all, IPMSG_BR_EXIT, Doe.nick);
	refresh(true);
	// 向所有在线好友发送离线消息。
	// ..................
	// sendMsg(me, EVERYONE, IPMSG_BR_EXIT, me.nick);

	if (close_counter < 2)
	    close_counter++;
	else
	    quit_app (true);
	return true;
    }

    function quit_app (forcibly) {

	if (typeof nw === 'object') { // nwjs 的情况

	    // Wait a moment to avoid segmentation fault (core dump).
	    setTimeout (() => {
		// 再检查一次, 若 UPnP 仍无响应则退出
		if (forcibly || quit_ongoing <= 1)
		    nw.App.quit();
	    }, 500);
	    // 万一退出失败，再试另一个退出方法
	    setTimeout (function () {
		// 再检查一次, 若 UPnP 仍无响应则退出
		if (forcibly || quit_ongoing <= 1)
		    process.exit();
	    }, 1000);
	} else { // electron 的情况

	    //这一句写在此处用处不大，写在 main.js 里面才起作用。
	    //e.preventDefault();

	    // Wait a moment to avoid segmentation fault (core dump).
	    // 用 app.quit() 不行，它会再次触发 close 事件。
	    setTimeout (() => {
		// 再检查一次, 若 UPnP 仍无响应则退出
		if (forcibly || quit_ongoing <= 1) {
		    require('electron').ipcRenderer.send ('quit-app');
		}
	    }, 500);
	}

    }

// wget-improved 在下载失败时不能停止下载, 只好试试 node-downloader-helper

	function download_failure (err, file) {
		fail('文件 ' + file + ' 接收失败! ' + err);
		setTimeout(update_file_real, 0);
	}

	function download_success (file, f_download, f_log, r_time, PalDir) {
		// 重命名 file 为临时文件名
		if (fs.existsSync(file)) {
		    // 先创建临时目录 h, 再删除它, 从而保证这个名字是
		    // 不存在的文件名或目录名
		    let basename = path.basename(file);
		    let g = path.resolve(PalDir, basename) + '.';
		    let h;
		    try {
			// 在用户专用目录下保存文件的旧版本
			h = fs.mkdtempSync(g);
		    } catch (e) {
			fail('创建临时目录 ' + g +
			    '?????? 时失败! 请手动处理! ' + e);
			fail('请删除或更名 ' + file +', 然后设法将 ' +
			    f_download + ' 更名为 ' + file +
			    ' 并将其修改时间设定为 .log 的修改时间。');
			// 文件更新完成, 允许更新下一个文件
			//if (! instantly) file_updating = false;
			// 此处保留 f_log 文件, 不要删除
			setTimeout(update_file_real, 0);
			return;
		    }
		    // Windows 下需要先删除 h 才能重命名为 h
		    try {
			fs.rmdirSync(h);
		    } catch (e) {
			fail('删除临时目录 ' + h
				+ ' 时失败! 请手动处理! ' + e);
			fail('请删除或更名 ' + file +', 然后设法将 ' +
			    f_download + ' 更名为 ' + file +
			    ' 并将其修改时间设定为 .log 的修改时间。');
			// 文件更新完成, 允许更新下一个文件
			//if (! instantly) file_updating = false;
			// 此处保留 f_log 文件, 不要删除
			setTimeout(update_file_real, 0);
			return;
		    }
		    try {
			fs.renameSync(file, h);
		    } catch (e) {
			fail('把路径 ' + file + ' 重命名为 ' +
				h + ' 时失败! 请手动处理! ' + e);
			fail('请删除或更名 ' + file +', 然后设法将 ' +
			    f_download + ' 更名为 ' + file +
			    ' 并将其修改时间设定为 .log 的修改时间。');
			// 文件更新完成, 允许更新下一个文件
			//if (! instantly) file_updating = false;
			// 此处保留 f_log 文件, 不要删除
			setTimeout(update_file_real, 0);
			return;
		    }
		}
		// 更名 f_download 为 file
		try {
		    fs.renameSync(f_download, file);
		} catch (e) {
		    fail('更名 ' + f_download + ' 为 ' + file +
				' 时失败! 请手动处理! ' + e);
		    fail('请设法将 ' + f_download + ' 更名为 ' + file +
			' 并将其修改时间设定为 .log 的修改时间。');
		    // 文件更新完成, 允许更新下一个文件
		    //if (! instantly) file_updating = false;
		    // 此处保留 f_log 文件, 不要删除
		    setTimeout(update_file_real, 0);
		    return;
		}

		// 修正 file 的 mtime
		try {
		    fs.utimesSync(file, r_time, r_time);
		} catch (e) {
		    fail('改变 ' + file + ' 的 mtime 失败! ' + e);
		    fail('接收文件 ' + file +
			' 成功, 但无法同步文件修改时间, 需手动操作! ' +
			'请设法将其修改时间设定为 .log 的修改时间。');
		    // 文件更新完成, 允许更新下一个文件
		    //if (! instantly) file_updating = false;
		    // 此处保留 f_log 文件, 不要删除
		    setTimeout(update_file_real, 0);
		    return;
		}
		// 文件更新完成, 允许更新下一个文件
		//if (! instantly) file_updating = false;
		fail('完成接收 ' + file);
		setTimeout(update_file_real, 0);
		// 删除 f_log
		if (fs.existsSync(f_log)) {
		    let stat = fs.statSync(f_log);
		    if (! stat.isFile()) {
			fail('出现异常! 文件 ' + f_log +
				' 不是普通文件! 它本应是普通文件! 请留意! ');
			return; // 不执行删除
		    }
		    if (stat.size) {
			fail('出现异常! 文件 ' + f_log +
				' 不是空文件 (长度 ' + stat.size +
				' 字节)! 它本应是空文件! 请留意! ');
			return; // 不执行删除
		    }
		    let t = Math.floor(stat.mtime.getTime() / 1000) & (-2);
		    if (t != r_time) {
			fail('出现异常! 文件 ' + f_log +
				' 的修改时间与远程文件不同, 请留意! ');
			return; // 不执行删除
		    }
		    if (! fs.existsSync(file)) {
			fail('出现异常! 刚刚下载的文件 ' + file +
				' 却消失了! 请留意! ');
			return; // 不执行删除
		    }
		    stat = fs.statSync(file);
		    t = Math.floor(stat.mtime.getTime() / 1000) & (-2);
		    if (t != r_time) {
			fail('出现异常! 文件 ' + file +
				' 的修改时间与远程文件不同, 请留意! ');
			return; // 不执行删除
		    }
		    try {
			fs.unlinkSync(f_log);
		    } catch (e) {
			fail('删除 ' + f_log + ' 失败: ' + e);
		    }
		} else {
		    fail('出现异常! 下载完成后正准备删除文件 ' + f_log +
				' 时, 此文件已经不存在了! 请留意! ');
		    return;
		}
	}

    function download_file (file, f_download, f_log, r_time,
			download_start, src, PalDir) {
	const { byteHelper } =
		require('./node_modules/node-downloader-helper/bin/helpers');
	const { DownloaderHelper, DH_STATES } =
		require('./node_modules/node-downloader-helper/dist');
	const pkg =
		require('./node_modules/node-downloader-helper/package.json');
	const Url = src; // 'https://proof.ovh.net/files/1Gb.dat';

	const pauseResumeTimer = (_dl, wait) => {
	    setTimeout(() => {
		if (_dl.state === DH_STATES.FINISHED ||
		    _dl.state === DH_STATES.FAILED) {
		    return;
		}

		_dl.pause()
		    .then(() => fail('暂停 ' + (wait / 1000) + ' 秒'))
		    .then(() => setTimeout(() => _dl.resume(), wait));

	    }, wait);
	};

	const options = {
	    method: 'GET', // Request Method Verb
	    // Custom HTTP Header ex: Authorization, User-Agent
	    headers: {
	        'user-agent': pkg.name + '/' + pkg.version
	    },
	    retry: false,	// { maxRetries: 3, delay: 3000 }
				// { maxRetries: number, delay: number in ms }
				// or false to disable (default)
	    fileName: f_download,
			// filename => filename + '.gz'
			// Custom filename when saved
	    //  override 可以是
	    //  object:
	    //		{
	    //			skip: skip if already exists,
	    //			skipSmaller: skip if smaller
	    //		}
	    //  或
	    //  boolean:
	    //		true to override file
	    //		false to append '(number)' to new file name
	    override: true, // { skip: true, skipSmaller: true },
	    resumeOnIncomplete: true,
	    resumeOnIncompleteMaxRetry: 5,
	    resumeIfFileExists: false,
	    forceResume: false, // If server does not return "accept-ranges"
				// in header but it does support it
	    removeOnStop: false, // remove file when stopped (default:true)
	    removeOnFail: false, // remove file when fail (default:true)    
	    httpRequestOptions: {}, // Override the http request options  
	    httpsRequestOptions: {} // Override the https request options,
				    // ex: to add SSL Certs
	};

	//let startTime = new Date();
	let mydir = path.dirname(f_download);
	//const dl = new DownloaderHelper(Url, __dirname, options);
	const dl = new DownloaderHelper(Url, mydir);

	dl.once('download', () => pauseResumeTimer(dl, 5000));
	//dl.on('download', downloadInfo => {
	//	//fail('Download Begins: ' +
	//	//	' name: ' + downloadInfo.fileName +
	//	//	', total: ' + downloadInfo.totalSize);
	//});
	dl.on('end', downloadInfo => {
		//fail('Download Completed: ' + JSON.stringify(downloadInfo));
		download_success (file, f_download, f_log, r_time, PalDir);
	});
	dl.on('skip', skipInfo => {
		fail('Download skipped. File already exists: ' + skipInfo);
	});
	dl.on('error', err => {
		fail('出错了! ' + err + ' getOptions: ' +
			JSON.stringify(dl.getOptions()));
		download_failure (err, file);
	});
	dl.on('retry', (attempt, opts, err) => {
		fail(
			'RetryAttempt: ' + attempt + '/' + opts.maxRetries +
			', StartsOn: ' + (opts.delay / 1000) + ' secs' +
			', Reason: ' + (err ? err.message : 'unknown')
		);
	});
	dl.on('resume', isResumed => {
		// if resume is not supported,  
		// a new pipe instance needs to be attached
		if (!isResumed) {
			// dl.unpipe();
			// dl.pipe(zlib.createGzip());
			fail("服务器不支持断点续传, 从头开始下载! ");
		}
	});
	dl.on('stateChanged', state => {
		// fail('State: ' + state);
	});
	dl.on('renamed', filePaths => {
		fail('File Renamed to: ' + filePaths.fileName);
	});
	dl.on('redirected', (newUrl, oldUrl) => {
		fail('Redirect from ' + newUrl + ' => ' + oldUrl);
	});
//	dl.on('progress', stats => {
//        const progress = stats.progress.toFixed(1);
//        const speed = byteHelper(stats.speed);
//        const downloaded = byteHelper(stats.downloaded);
//        const total = byteHelper(stats.total);
//
//        // print every one second (`progress.throttled` can be used instead)
//        const currentTime = new Date();
//        const elaspsedTime = currentTime - startTime;
//        if (elaspsedTime > 1000) {
//            startTime = currentTime;
//            fail(`${speed}/s - ${progress}% [${downloaded}/${total}]`);
//        }
//	});

	dl.resumeFromFile(f_download).catch(err => {
	    // already listening on 'error' event but catch can be used too
	    fail('Catched error: ' + err + ' getOptions: ' +
	    		JSON.stringify(dl.getOptions()));
	    download_failure (err, file);
	});

    }

//wget.js from wget-improved: https://github.com/bearjaws/node-wget
//'use strict';

const http = require('http');
//const https = require('https');
//const tunnel = require('../../tunnel');
//const url = require('url');
//const zlib = require('zlib');
//const fs = require('fs');
const EventEmitter = require('events').EventEmitter;

//const redirectCodes = [301, 302, 303, 307];
//const validCodes = [200, 201, 206];
//const handledCodes = validCodes.concat(redirectCodes);

// Downloads a file using http get and request
// @param {string} src - The http URL to download from
// @param {string} output - The filepath to save to
// @param {object} options - Options object
// @param {object} parentEvent - Used for when there is a 302 redirect and
//				 need to maintain state to a new request
// @param {number} redirects - The number of redirects, used to prevent
//				infinite loops
// @returns {*|EventEmitter}

function wget_download(src, output, options //, parentEvent, redirects = 0
			) {
    let downloader = //parentEvent ||
			new EventEmitter();
    //if (options) {
    //    options = parseOptions('download', options);
    //} else {
    //    options = {
    //        gunzip: false
    //    };
    //}

    //let srcUrl = url.parse(src);
    //if (!srcUrl.protocol || srcUrl.protocol === null) {
    //    downloader.emit(
    //        'error',
    //        'Cannot parse url protocol for ' +
    //            src +
    //            ', please specify either HTTP or HTTPS'
    //    );
    //    return false;
    //}
    //srcUrl.protocol = cleanProtocol(srcUrl.protocol);

    let req = http.request(
        {
            //protocol: options.protocol, // || srcUrl.protocol,
            host: options.host, // || srcUrl.hostname,
            port: options.port, // || srcUrl.port,
            path: options.path, // || srcUrl.pathname + (srcUrl.search || ''),
            //proxy: options.proxy ? options.proxy : undefined,
            //auth: options.auth ? options.auth : undefined,
	    headers: options.headers,
            method: 'GET'
        },
        function(res) {
            let downloadedSize;
        //  let gunzip = zlib.createGunzip();

	//  // Handle all common redirects
	//  if (redirectCodes.includes(res.statusCode)) {
	//	let newLocation = res.headers.location;
	//	// This enables support for relative redirects #11
	//	if (res.headers.location.indexOf('/') === 0) {
	//	    newLocation = srcUrl.protocol + '://' + srcUrl.hostname +
	//			(srcUrl.port ? ':' + srcUrl.port : '') +
	//			res.headers.location;
	//	}

	//	redirects++;
	//	if (redirects >= 10) {
	//	    downloader.emit('error', 'Infinite redirect loop detected');
	//	    return false;
	//	}
	//	download(newLocation, output, options, downloader, redirects);
	//  }
            //let fileSize;
            let writeStream;
            if (res.statusCode === 200 || res.statusCode === 206) {
                downloadedSize = 0;
                //fileSize = Number(res.headers['content-length']);

                // If content length header is not sent, there is no way to
		// determine file size.
                //if (isNaN(fileSize) === true) {
                //    fileSize = null;
                //}
                writeStream = fs.createWriteStream(output, {
                    flags: 'a+',
                    encoding: 'binary'
                });

                res.on('error', function(err) {
                    writeStream.end();
                    downloader.emit('error', err);
                });

                //let encoding = '';
                //if (typeof res.headers['content-encoding'] === 'string') {
                //    encoding = res.headers['content-encoding'];
                //}

                //// If the user has specified to unzip, and the file is gzip
		////  encoded, pipe to gunzip
                //if (options.gunzip === true && encoding === 'gzip') {
                //    res.pipe(gunzip);
                //} else {
                    res.pipe(writeStream);
                //}

                ////emit a start event so user knows file-size
                //downloader.emit('start', fileSize);

                // Data handlers
                res.on('data', function(chunk) {
                    downloadedSize += chunk.length;
                    //downloader.emit(
                    //    'progress',
                    //    calculateProgress(fileSize, downloadedSize)
                    //);
                    //downloader.emit('bytes', downloadedSize);
                });
                //gunzip.on('data', function(chunk) {
                //    writeStream.write(chunk);
                //});

                writeStream.on('finish', function() {
                    writeStream.end();
                    //downloader.emit('progress', 1);
                    downloader.emit('end', 'Finished writing to disk');
                });
            } else //if (!handledCodes.includes(res.statusCode))
	    {
                downloader.emit(
                    'error',
                    'Server responded with unhandled status: ' + res.statusCode
                );
            }
        }
    );

    req.flushHeaders();
    req.on('error', function(err) {
        downloader.emit('error', err);
    });
    // Attach request to our EventEmitter for backwards compatibility,
    // enables actions such as
    // req.abort();
    downloader.req = req;

    return downloader;
}

//function request(options, callback) {
//    //var newOptions = {},
//    //    newProxy = {},
//    //    key;
//    //options = parseOptions('request', options);
//    if (options.protocol === 'http') {
//        //if (options.proxy) {
//            //for (key in options.proxy) {
//            //    if (key !== 'protocol') {
//            //        newProxy[key] = options.proxy[key];
//            //    }
//            //}
//            //if (options.proxy.protocol === 'http') {
//            //    options.agent = tunnel.httpOverHttp({ proxy: newProxy });
//            //} else if (options.proxy.protocol === 'https') {
//            //    options.agent = tunnel.httpOverHttps({ proxy: newProxy });
//            //} else {
//        //        throw options.proxy.protocol + ' proxy is not supported!';
//            //}
//        //}
//        //for (key in options) {
//        //    if (key !== 'protocol' && key !== 'proxy') {
//        //        newOptions[key] = options[key];
//        //    }
//        //}
//        //return http.request(newOptions, callback);
//	delete(options.protocol);
//        return http.request(options, callback);
//    }
//    if (options.protocol === 'https') {
//        //if (options.proxy) {
//            //for (key in options.proxy) {
//            //    if (key !== 'protocol') {
//            //        newProxy[key] = options.proxy[key];
//            //    }
//            //}
//            //if (options.proxy.protocol === 'http') {
//            //    options.agent = tunnel.httpsOverHttp({ proxy: newProxy });
//            //} else if (options.proxy.protocol === 'https') {
//            //    options.agent = tunnel.httpsOverHttps({ proxy: newProxy });
//            //} else {
//        //        throw options.proxy.protocol + ' proxy is not supported!';
//            //}
//        //}
//        //for (key in options) {
//        //    if (key !== 'protocol' && key !== 'proxy') {
//        //        newOptions[key] = options[key];
//        //    }
//        //}
//        //return https.request(newOptions, callback);
//	delete(options.protocol);
//        return https.request(options, callback);
//    }
//    //throw 'Your URL must use either HTTP or HTTPS.';
//}

//function parseOptions(type, options) {
    //var proxy;
    //if (type === 'download') {
        //if (options.proxy) {
            //if (typeof options.proxy === 'string') {
            //    proxy = url.parse(options.proxy);
            //    options.proxy = {};
            //    options.proxy.protocol = cleanProtocol(proxy.protocol);
            //    options.proxy.host = proxy.hostname;
            //    options.proxy.port = proxy.port;
            //    options.proxy.proxyAuth = proxy.auth;
            //    options.proxy.headers = { 'User-Agent': 'Node-Wget' };
            //}
        //}
        //return options;
    //}
    //if (type === 'request') {
        //if (!options.protocol) {
        //    options.protocol = 'http';
        //}
        //options.protocol = cleanProtocol(options.protocol);

        //if (options.proxy) {
            //if (typeof options.proxy === 'string') {
            //    proxy = url.parse(options.proxy);
            //    options.proxy = {};
            //    options.proxy.protocol = cleanProtocol(proxy.protocol);
            //    options.proxy.host = proxy.hostname;
            //    options.proxy.port = proxy.port;
            //    options.proxy.proxyAuth = proxy.auth;
            //    options.proxy.headers = { 'User-Agent': 'Node-Wget' };
            //}
        //}

        //options.gunzip = options.gunzip || false;
        //return options;
    //}
//}

//function cleanProtocol(str) {
//    return str
//        .trim()
//        .toLowerCase()
//        .replace(/:$/, '');
//}

//function calculateProgress(fileSize, totalDownloaded) {
//    // If we don't know the size of the download, we Microsoft(tm) the
//		progress bar into a moving target...
//    if (fileSize === null) {
//        const length = String(totalDownloaded).length;
//        // This guarantees that totalDownloaded is never greater than or
//		equal to fileSize.
//        fileSize = Math.pow(10, length) + 1;
//    }
//
//    return totalDownloaded / fileSize;
//}

//exports.download = download;
//exports.request = request;
// end of wget.js

//  const wget = require('./node_modules/wget-improved');
//    const wget = { download: download, request: request };

    let update_file_queue = [];
    function update_file (id, paldir, f, instantly, log_only) {
	update_file_queue.push([id, paldir, f, log_only]);
	update_file_loop();
    }
    let file_updating = false;
    function update_file_loop () {
	if (file_updating)
	    return;
	file_updating = true;
	update_file_real();
    }
    function update_file_real () {
	if (! update_file_queue.length) {
	    file_updating = false;
	    return;
	}
	//const instantly = false;
	let id = update_file_queue[0][0];
	let paldir = update_file_queue[0][1];
	let f = update_file_queue[0][2];
	let log_only = update_file_queue[0][3];
	update_file_queue.shift(); //  删除数组的第一个元素

	let filepath = encodeURI('/' + me.nick + stored_randomID + '/' + f
			.replace(/\\+/g, '/')
			.replace(/\/{2,}/, '/'));
	let PalDir = path.resolve('..', 'wwwroot', paldir);
	let file = path.resolve(PalDir, f);
	if (! mkdir_recursive(PalDir)) {
	    fail (' 创建文件夹 ' + PalDir + ' 失败! 无法接收文件 ' + file);
	    //if (! instantly) file_updating = false;
	    setTimeout(update_file_real, 0);
	    return;
	}
	fail('正在接收 ' + file);
	let f_download = file + '.download';
	let f_log = file + '.log';
	// 探测远程文件 mtime
	let options = {};
	//options.protocol = 'http:';
	options.host = Pal[id].IP;
	options.port = 5080;
	options.path = filepath;
	options.method = 'HEAD';
	let req = http.request(options, function(res) {
	    //fail('response:<br/>' + JSON.stringify(res.headers));
	    //let content = '';
	    if (res.statusCode === 200) {
		res.on('error', function(err) {
		    // 不存在远程文件
		    //if (! instantly) file_updating = false;
		    fail('文件 ' + file + ' 接收失败: ' + err);
		    setTimeout(update_file_real, 0);
		});
		res.on('data', function(chunk) {
		    //content += chunk;
		});
		res.on('end', function() {
		    if (! /\bbytes\b/.test(res.headers['accept-ranges'])) {
			//if (! instantly) file_updating = false;
			fail('接收文件 ' + file +
				' 失败: 服务器不支持断点续传');
			setTimeout(update_file_real, 0);
			return;
		    }
		    if (res.headers['content-length'] == undefined) {
			//if (! instantly) file_updating = false;
			fail('接收文件 ' + file + ' 失败: 远程文件是目录');
			setTimeout(update_file_real, 0);
			return;
		    }
		    if (res.headers['content-length'] == 0) {
			//if (! instantly) file_updating = false;
			fail('接收文件 ' + file + ' 失败: 远程文件长度为 0');
			setTimeout(update_file_real, 0);
			return;
		    }
		    if (res.headers['last-modified'] == undefined) {
			//if (! instantly) file_updating = false;
			fail('接收文件 ' + file + ' 失败: 远程文件无修改时间');
			setTimeout(update_file_real, 0);
			return;
		    }
		    let r_size = res.headers['content-length'];
		    let r_time = Math.floor(
				(new Date(res.headers['last-modified']))
				.getTime() / 1000) & (-2);
//fail('接收文件 ' + file + ' : 远程文件 ' + r_size + ', ' + r_time + ', ' + res.headers['last-modified']);

		    // 检查本地文件 file 的长度和时间
		    let already_syncked = false;
		    if (fs.existsSync(file)) {
			let stat = fs.statSync(file);
			//let l_size = stat.size;
			let l_time =
			    Math.floor(stat.mtime.getTime() / 1000) & (-2);
			if (r_size == stat.size && r_time == l_time)
				already_syncked = true;
//fail('接收文件 ' + file + ' : 本地文件 ' + stat.size + ', ' + l_time + (already_syncked ? ' 已同步' : ' 未同步'));
		    }
		    if (already_syncked && ! log_only) {
			//if (! instantly) file_updating = false;
			//fail('文件 ' + file + ' 是同步的, 无需更新');
			setTimeout(update_file_real, 0);
			return;
		    }

//fail('接收文件 ' + file + ' : 检查 log 文件...');
		    // 检查 .log 文件
		    
		    if (fs.existsSync(f_log)) {
			let stat = fs.statSync(f_log);
			if (! stat.isFile()) {
			    //if (! instantly) file_updating = false;
			    fail('接收文件 ' + file +
			    	' 失败: 本地 .log 文件不是普通文件');
			    setTimeout(update_file_real, 0);
			    return;
			}
			if (stat.size) {
			    //if (! instantly) file_updating = false;
			    fail('接收文件 ' + file +
			    	' 失败: 本地 .log 文件不是空文件 (' +
				stat.size + ' 字节)');
			    setTimeout(update_file_real, 0);
			    return;
			}
			let t = Math.floor(stat.mtime.getTime() / 1000) & (-2);
			if (t != r_time) {
			    // 删除 f_download 文件
			    if (fs.existsSync(f_download)) {
				try {
				    fs.unlinkSync(f_download);
				} catch (e) {
				    //if (! instantly) file_updating = false;
				    fail('删除 ' + f_download + ' 失败: ' + e);
				    fail('接收文件 ' + file +
				    	' 失败: 无法删除旧的 .download 文件');
				    setTimeout(update_file_real, 0);
				    return;
				}
			    }
			    // 修改 f_log 的 mtime
			    try {
				fs.utimesSync(f_log, r_time, r_time);
			    } catch (e) {
				//if (! instantly) file_updating = false;
				fail('改变 ' + f_log + ' 的 mtime 失败! ' + e);
				fail('接收文件 ' + file +
					' 失败: 无法修改 .log 文件的 mtime');
				setTimeout(update_file_real, 0);
				return;
			    }
			}
		    } else {
			// 删除 f_download 文件
			if (fs.existsSync(f_download)) {
			    try {
				fs.unlinkSync(f_download);
			    } catch (e) {
				//if (! instantly) file_updating = false;
				fail('删除 ' + f_download + ' 失败: ' + e);
				fail('接收文件 ' + file +
					' 失败: 无法删除旧的 .download 文件');
				setTimeout(update_file_real, 0);
				return;
			    }
			}
			// 创建 f_log 文件
			try {
			    fs.appendFileSync(f_log, '');
			} catch (e) {
			    //if (! instantly) file_updating = false;
			    fail('创建文件 ' + f_log +
				' 时出现意外, 无法接收文件! ' + e);
			    fail('接收文件 ' + file +
				' 失败: 无法创建 .log 文件');
			    setTimeout(update_file_real, 0);
			    return;
			}
			// 设定 f_log 的 mtime
			try {
			    fs.utimesSync(f_log, r_time, r_time);
			} catch (e) {
			    //if (! instantly) file_updating = false;
			    fail('改变 ' + f_log + ' 的 mtime 失败! ' + e);
			    fail('接收文件 ' + file +
				' 失败: 无法设定 .log 文件的 mtime');
			    setTimeout(update_file_real, 0);
			    return;
			}
		    }

//fail('接收文件 ' + file + ' : log_only = ' + log_only + ', already_syncked = ' + already_syncked);
		    // 此时 .log 已存在, 且有正确的 mtime

		    if (log_only) {
			//if (! instantly) file_updating = false;
			//fail('已更新文件 ' + f_log + ' 的修改时间');
			setTimeout(update_file_real, 0);
			return;
		    }

		    if (already_syncked) {
			//if (! instantly) file_updating = false;
			//fail('文件 ' + file + ' 是同步的, 无需更新');
			setTimeout(update_file_real, 0);
			return;
		    }

//fail('接收文件 ' + file + ' : 检查 download 文件...');
		    // 检查 .download 文件

		    let download_start = 0;
		    if (fs.existsSync(f_download)) {
			let stat = fs.statSync(f_download);
			if (! stat.isFile()) {
			    //if (! instantly) file_updating = false;
			    fail('接收文件 ' + file +
			    	' 失败: 本地 .download 文件不是普通文件');
			    setTimeout(update_file_real, 0);
			    return;
			}
			if (stat.size >= r_size) {
			    // 文件长度已达到或超过远程文件的长度, 表明无效
			    // 删除 f_download 文件
			    if (fs.existsSync(f_download)) {
				try {
				    fs.unlinkSync(f_download);
				} catch (e) {
				    //if (! instantly) file_updating = false;
				    fail('删除 ' + f_download + ' 失败: ' + e);
				    fail('接收文件 ' + file +
				    	' 失败: 无法删除旧的 .download 文件');
				    setTimeout(update_file_real, 0);
				    return;
				}
			    }
			} else {
			    download_start = stat.size;
			}
		    }
//fail('接收文件 ' + file + ' : 准备下载 download 文件...');
		    // 这个网页
		    // https://www.devdungeon.com/content/using-wget-nodejs
		    // 提到，wget.download() 函数在下载时不覆盖文件，而是追加,
		    // 但当前的 wget-improved 3.4.0 代码却使用 w+ 标志来打开
		    // 文件, 这说明已经是覆盖，而不是追加了。而且, 代码中
		    // 不存在 resume、break-point、range 等字眼，因此可以肯定
		    // 不支持断点续传。
		    // 找到了一个支持断点续传的 project:
		    // https://github.com/hyperzlib/nodejs-breakpoint-download
		    // 代码本身还不错, 但已有 6 年未更新了, 而且只有一个 star
		    // 和一个 fork, 这也难以让人有足够的信心来采用。
		    // 那些蹭 wget 热度的 npm 项目都不支持断点续传。唯一一个
		    // 支持断点续传的 npm 项目，却不带 wget 字样, 这说明作者
		    // 不打算把软件做大做强, 大概率会半途而废。
		    // 现在先尝试给 wget-improved 打补丁, 让它支持断点续传。
		    // 哦不! 又找到一个 gotResume, 上次更新是 8 个月前。它
		    // 也没蹭 wget 的热度, 但它看起来很严谨、有后劲。
		    // got 有惊人的 fork、star 和下载量。老天捉弄人! 我已经
		    // 误上了 wget-improved 的贼船! 唉! 实在没办法, 还是硬着
		    // 头皮给 wget-improved 打补丁吧, 万一失败再转战 got。
		    // wget-improved 出现了一个小问题, 在下载失败时失去响应,
		    // 而不是 timeout, (可能是我不懂如何监听 timeout 事件吧),
		    // 最后，用 node-downloader-helper 来搞, 应该没问题了。还
		    // 找到了 https://www.npmjs.com/package/download 备用。

		    // download_start 已经确定, 现在只需从此处开始断点续传。
		    // 续传完成时, 应执行
		    //		file_updating = false;
		    //		fail('文件接收完成: ' + file);
		    // 并把 f_download 更名为 file 以及修正 mtime。


		    // 原始文件名 file
		    // 下载文件名 f_download
		    // 断点续传位置 download_start
		    // host:	Pal[id].IP
		    // path:	filepath

		    const src = 'http://' + (/:/.test(Pal[id].IP) ?
				'[' + Pal[id].IP + ']' : Pal[id].IP) +
				':5080' + filepath;
		    download_file(file, f_download, f_log, r_time,
					download_start, src, PalDir);
		    return; // 屏蔽掉后续的 wget 代码
		    // 下面的 wget 代码不知哪里有错。能下载成功, 更名也成功,
		    // 删除 log 文件也成功, 但奇怪的是, 文件的修改时间未修正
		    const output = f_download;
		    const options = {
			//protocol:	'http:',
			host:		Pal[id].IP,
			port:		5080,
			path:		filepath,
			headers: {
				Range: 'bytes=' + download_start + '-',
			}
		    };

		    let download = wget_download(src, output, options);
		    download.on('error', function(err) {
			//if (! instantly) file_updating = false;
			fail('文件 ' + file + ' 接收失败! ' + err);
			setTimeout(update_file_real, 0);
		    });
		//  download.on('start', function(fileSize) {
		//	//console.log(fileSize);
		//  });
		    download.on('end', function(output) {
			// 重命名 file 为临时文件名
			if (fs.existsSync(file)) {
			    // 先创建临时目录 h, 再删除它, 从而保证这个名字是
			    // 不存在的文件名或目录名
			    let basename = path.basename(file);
			    let g = path.resolve(PalDir, basename) + '.';
			    let h;
			    try {
				// 在用户专用目录下保存文件的旧版本
				h = fs.mkdtempSync(g);
			    } catch (e) {
				fail('创建临时目录 ' + g +
				    '?????? 时失败! 请手动处理! ' + e);
				fail('请删除或更名 ' + file +', 然后设法将 ' +
				    f_download + ' 更名为 ' + file +
				    ' 并将其修改时间设定为 .log 的修改时间。');
				// 文件更新完成, 允许更新下一个文件
				//if (! instantly) file_updating = false;
				// 此处保留 f_log 文件, 不要删除
				setTimeout(update_file_real, 0);
				return;
			    }
			    // Windows 下需要先删除 h 才能重命名为 h
			    try {
				fs.rmdirSync(h);
			    } catch (e) {
				fail('删除临时目录 ' + h
					+ ' 时失败! 请手动处理! ' + e);
				fail('请删除或更名 ' + file +', 然后设法将 ' +
				    f_download + ' 更名为 ' + file +
				    ' 并将其修改时间设定为 .log 的修改时间。');
				// 文件更新完成, 允许更新下一个文件
				//if (! instantly) file_updating = false;
				// 此处保留 f_log 文件, 不要删除
				setTimeout(update_file_real, 0);
				return;
			    }
			    try {
				fs.renameSync(file, h);
			    } catch (e) {
				fail('把路径 ' + file + ' 重命名为 ' +
					h + ' 时失败! 请手动处理! ' + e);
				fail('请删除或更名 ' + file +', 然后设法将 ' +
				    f_download + ' 更名为 ' + file +
				    ' 并将其修改时间设定为 .log 的修改时间。');
				// 文件更新完成, 允许更新下一个文件
				//if (! instantly) file_updating = false;
				// 此处保留 f_log 文件, 不要删除
				setTimeout(update_file_real, 0);
				return;
			    }
			}
			// 更名 f_download 为 file
			try {
			    fs.renameSync(f_download, file);
			} catch (e) {
			    fail('更名 ' + f_download + ' 为 ' + file +
					' 时失败! 请手动处理! ' + e);
			    fail('请设法将 ' + f_download + ' 更名为 ' + file +
				' 并将其修改时间设定为 .log 的修改时间。');
			    // 文件更新完成, 允许更新下一个文件
			    //if (! instantly) file_updating = false;
			    // 此处保留 f_log 文件, 不要删除
			    setTimeout(update_file_real, 0);
			    return;
			}

			// 修正 file 的 mtime
			try {
			    fs.utimesSync(file, r_time, r_time);
			} catch (e) {
			    fail('改变 ' + file + ' 的 mtime 失败! ' + e);
			    fail('接收文件 ' + file +
				' 成功, 但无法同步文件修改时间, 需手动操作! ' +
				'请设法将其修改时间设定为 .log 的修改时间。');
			    // 文件更新完成, 允许更新下一个文件
			    //if (! instantly) file_updating = false;
			    // 此处保留 f_log 文件, 不要删除
			    setTimeout(update_file_real, 0);
			    return;
			}
			// 文件更新完成, 允许更新下一个文件
			//if (! instantly) file_updating = false;
			fail('完成接收 ' + file);
			setTimeout(update_file_real, 0);
			// 删除 f_log
			if (fs.existsSync(f_log)) {
			    try {
				fs.unlinkSync(f_log);
			    } catch (e) {
				fail('删除 ' + f_log + ' 失败: ' + e);
			    }
			}
		    });
		    //download.on('progress', function(progress) {
			//    typeof progress === 'number'
			// code to show progress bar
		    //});
		});
	    } else {
		//if (! instantly) file_updating = false;
		fail('接收文件 ' + file + ' 时服务器响应异常。状态码: ' +
			res.statusCode);
		//fail('options:<br/>' + JSON.stringify(options));
		//fail('response:<br/>' + JSON.stringify(res.headers));
		setTimeout(update_file_real, 0);
	    }
	});

	req.end();
	req.on('error', function(err) {
	    //if (! instantly) file_updating = false;
	    fail('接收文件 ' + file + ' 时服务器出错: ' + err);
	    setTimeout(update_file_real, 0);
	});

    }

    let SyncList = {};
    let askSyncIntervalID = {};

    function dir_sync() {
	let update_file_count = 0;
	for (let id in Pal) {
	    // 如果没有 ip, 跳过
	    if (! Pal[id].IP || ! Pal[id].port) // 不存在 ip 或 port
		continue; // 继续检查下一个用户
	    if (! Pal[id].haskey) // 不是 miao 用户
		continue; // 继续检查下一个用户
	    if (Pal[id].offline) // 已离线
		continue; // 继续检查下一个用户
	    if (/:/.test(Pal[id].IP) && Pal[id].haskey <= 0)
		continue; // IPv6 用户不是好友, 继续检查下一个用户
	    let palDirName = Pal[id].nick +
		id.replace(/@.*$/,'').replace(/:/g,'_');
	    // 不给自己发送同步文件夹
	    if (palDirName == me.nick + stored_randomID)
		continue; // 继续检查下一个用户

	    // 先更新对方同步信息文件
	    let f = palDirName + '.txt';
	    let yourSyncDir = path.resolve('..', 'wwwroot', palDirName, f);
	    let yourDirText = '';
	    update_file(id, palDirName, f, true);

	    // 记录对方保存的我方同步信息文件的时间
	    f = me.nick + stored_randomID + '.txt';
	    let mySyncDir = path.resolve('..', 'wwwroot', palDirName, f);
	    let myDirText = '';
	    update_file(id, palDirName, f, true, true);

	    // 给用户发送 2 条探测消息, 期待对方回应在线
	    sendMsg(me, Pal[id], IPMSG_BR_ENTRY, me.nick);
	    setTimeout(sendMsg, 2000, me, Pal[id], IPMSG_BR_ENTRY, me.nick);
	    if (Date.now() - Pal[id].time > 60000) {
		if (Pal[id].time == 0) {
		    // 上次清零后未变动, 说明无响应
		    Pal[id].offline = 1; // 标记已离线
		    updateTable(id); // 用户列表更新离线信息
		    continue; // 继续检查下一个用户
		}
		// 用户在线状态未知, 清除用户响应时间, 作为标志
		Pal[id].time = 0;
		updateTable(id); // 用户列表更新 time 信息

		// 返回非零值, 指示需要下一轮文件夹同步检查
		return -1; // 就用一个负数吧
	    }

	    // 用户在线

	    if (fs.existsSync(yourSyncDir)) {
		try {
		    yourDirText = fs.readFileSync(yourSyncDir, 'utf8');
		} catch (e) {
		    fail ('读文件' + yourSyncDir + '失败! ' + e);
		}
	    }

//fail('-------------检查 sync 文件夹-------------');
	    // 检查 sync 文件夹

	    let d = path.join('..', 'wwwroot', palDirName, 'sync');

	    if (yourDirText) {
		if (! mkdir_recursive(d)) {
		    fail (' 创建文件夹 ' + d + ' 失败! 无法完成同步!');
		    return;
		}
	    }

	    if (! fs.existsSync(d)) // 不存在 sync
		continue; // 继续检查下一个用户
	    if (! fs.statSync(d).isDirectory()) // sync 不是文件夹
		continue; // 继续检查下一个用户

	    // 生成本机同步信息 myDirText
//fail('-------------生成本机同步信息到变量-------------');

	    let list_recursive = function (dir) {
		if (! dir)
		    dir = d;
		let files;
		try {
		    files = fs.readdirSync(dir);
		} catch (e) {
		    fail ('读目录' + dir + '失败! ' + e);
		    return;
		}
		let len = files.length;
		for (let j = 0; j < len; j++) {
		    if (files[j] == '.')
			continue;
		    if (files[j] == '..')
			continue;
		    let file = path.join(dir, files[j]);
		    let stat = fs.statSync(file);
		    if (stat.isDirectory()) {
			list_recursive (file);
			continue;
		    }
		    if (stat.size == 0) // 跳过长度为 0 的文件
			continue;
		    if (/:/.test(file)) // 跳过路径包含冒号的文件
			continue;
		    if (/\.download$/.test(file)) // 跳过 .download 文件
			continue;
		    // 格式: 文件长度 修改日期 文件相对路径
		    let f = path.relative(d, file).replace(/\\/g,'/');
		    let t = (Math.floor(stat.mtime.getTime() / 1000) & (-2));
		    let info = ':' + stat.size + ':' + t + ':' + f + '\r\n';
		    myDirText += info;
		}
	    };
	    list_recursive ();
	    if (! myDirText && ! yourDirText)
		continue; // 继续检查下一个用户

	    // 监视同步文件夹 d
	    if (! SyncList[palDirName]) {
		SyncList[palDirName] = fs.watch(d, (eventType, filename) => {
		    dir_sync_start();
		});
	    }

	    // 准备生成 mySyncDir 文件, 内容为 myDirText
//fail('-------------准备生成本机同步信息文件-------------');

	    label_mySyncDir:
	    if (myDirText)
	    if (fs.existsSync(mySyncDir)) {
		// 若存在 mySyncDir，比较其内容与 myDirText,
		// 若一致, 采用旧的 mySyncDir
		// 若不一致, 更新 mySyncDir
		let s = fs.statSync(mySyncDir);
		if (s.isFile() && s.size == new Blob([myDirText]).size) {
		    let r;
		    try {
			r = fs.readFileSync(mySyncDir, 'utf8');
		    } catch (e) {
			fail ('读文件' + mySyncDir + '失败! ' + e);
			continue; // 继续检查下一个用户
		    }
		    if (r == myDirText) {
	    		break label_mySyncDir; // 无需更新 mySyncDir
		    }
		}
		if (s.isDirectory()) {
		    // 更名 mySyncDir, 相当于删除它
		    let h;
		    try {
			h = fs.mkdtempSync(mySyncDir + '.');
		    } catch (e) {
			fail ('创建临时目录 ' + mySyncDir + '.* 失败: ' + e);
			continue; // 继续检查下一个用户
		    }
		    // Windows 下需要先删除 h 才能重命名为 h
		    try {
			fs.rmdirSync(h);
		    } catch (e) {
			fail ('删除临时目录 ' + h + ' 失败: ' + e);
			continue; // 继续检查下一个用户
		    }
		    try {
			fs.renameSync(mySyncDir, h);
		    } catch (e) {
			fail ('重命名 '+ mySyncDir +' 为 '+ h +' 失败: '+ e);
			continue; // 继续检查下一个用户
		    }
		} else {
		    try {
			fs.unlinkSync(mySyncDir); // 直接删除普通文件
		    } catch (e) {
			fail ('删除文件 ' + mySyncDir + ' 失败: ' + e);
			continue; // 继续检查下一个用户
		    }
		}
		// 创建 mySyncDir
		try {
		    fs.writeFileSync(mySyncDir, myDirText);
		} catch (e) {
		    fail ('写文件' + mySyncDir + '失败! ' + e);
		    continue; // 继续检查下一个用户
		}
	    } else {
		// 创建 mySyncDir
		try {
		    fs.writeFileSync(mySyncDir, myDirText);
		} catch (e) {
		    fail ('写文件' + mySyncDir + '失败! ' + e);
		    continue; // 继续检查下一个用户
		}
	    }

	    // 文件 mySyncDir 已经生成, 内容为 myDirText
//fail('-------------本机同步信息文件已生成-------------');

	    if (! fs.existsSync(yourSyncDir))
		continue; // 继续检查下一个用户
	    if (! fs.statSync(yourSyncDir).isFile())
		continue; // 继续检查下一个用户

	    // 更新文件
//fail('-------------更新文件-------------');

	    let lines = yourDirText.split('\r\n');
	    let file_change = false;
	    for (let i = 0; i < lines.length; i++) {
		if (! lines[i] && i + 1 == lines.length) // 结尾的空行
		    break; // 终止处理
		let info = lines[i].split(':');
		if (info.length != 4) { // 遇到错误的行
		    fail('同步信息文件 ' + yourSyncDir +
				' 含有无效行, 行号为 ' + (i + 1) + 
				', 此行及之后的行都视为无效! ');
		    break; // 终止处理
		}
		let n;
		let c;
		let t = info[2];
		let f = info[3];
		// 本机同步信息中是否含有对方的文件 f 的信息?
		let p = myDirText.indexOf(':' + f + '\r\n');
		if (p > 0) {
		    // 确实含有 f 的信息, 再查找时间域前的冒号位置 c
		    c = myDirText.lastIndexOf(':', p - 1);
		    // 这就得到本地文件的最后修改时间 n
		    n = Number(myDirText.slice(c + 1, p));
		    if (n >= t) // 本地文件不比对方文件旧
			continue; // 继续检查对方同步信息中的下一条记录
		}
		// 本机没有 f 文件, 或者 f 比对方文件旧, 因此需要更新
		update_file_count++;
		file_change = true;
//fail('即将更新 ' + f);
		update_file(id, palDirName, path.join('sync', f));
		//break; // 若用 break, 则只更新一个文件
	    }

//fail('---------更新任务已经派发完毕-------------');
	    if (! file_change) {
		// 更新完成, 循环检查 mySyncDir 和 mySyncDir+'.log' 的 mtime,
		// 若不同, 则每隔 5 分钟向对方发送“请求同步”的消息。
		let check_mtime = function () {
		    if (! myDirText || ! fs.existsSync(mySyncDir))
			return true;
		    let log = mySyncDir + '.log';
		    if (fs.existsSync(log)) {
			let s1 = fs.statSync(mySyncDir);
			let s2 = fs.statSync(log);
			let t1 = Math.floor(s1.mtime.getTime() / 1000) & (-2);
			let t2 = Math.floor(s2.mtime.getTime() / 1000) & (-2);
			if (t1 == t2)
			    return true;
		    }
		    // 发送“请求同步”消息
//fail('-------------向对方发送请求同步消息-------------');
		    sendMsg(me, Pal[id], IPMSG_CAPFILEENC_OBSLT, '');
		    return false;
		};
		if (check_mtime()) { 
		    if (askSyncIntervalID[id]) {
			clearInterval(askSyncIntervalID[id]);
			askSyncIntervalID[id] = undefined;
		    }
		} else if (! askSyncIntervalID[id]) {
		    askSyncIntervalID[id] = setInterval( () => {
			// 如果 mtime 测试通过, 则终止循环
			if (check_mtime() && askSyncIntervalID[id]) {
			    clearInterval(askSyncIntervalID[id]);
			    askSyncIntervalID[id] = undefined;
			}
		    }, 300000);
		}
	    }
//fail('update_file_count = ' + update_file_count);
	}
	return update_file_count;
    }

    let dir_sync_start_intervalID;

    function dir_sync_start () {
	// 每隔 30 秒执行一次同步, 直到同步完成
	if (dir_sync_start_intervalID)
	    return;
	dir_sync_start_intervalID = setInterval (() => {
	    // 如果所有用户的同步都已完成, 则终止循环
	    if (! dir_sync() && dir_sync_start_intervalID) {
		clearInterval(dir_sync_start_intervalID);
		dir_sync_start_intervalID = undefined;
	    }
	}, 30000);
    }

/////////////////////////////////////////////////////////////////////////////
//
// 用这个判断来区分两次不同的 open 操作。本 html 文件被第一次打开时，是作为
// 主窗口，此时 opener 为 null（在 electron 的情形）或 global（在 nwjs 的情
// 形）。随后在主窗口双击好友列表中的好友时, 会跳出聊天窗, 这个聊天窗就是第
// 二次打开本 html 文件后生成的, 此时 opener 就是主窗口了（聊天窗的打开者是
// 主窗口）。注意，electron 的主窗口是 global, 也就是说, 聊天窗的 opener 也
// 是 global, 这是个坑，先前用下面的代码就把聊天窗误判为主窗口:
//		if (opener == null || opener == global)
// 在 electron 的情形，聊天窗的 opener 是 global, 满足右半部分的判断，因此
// 被误判为主窗口。
//
// 情况对比	 主opener	 主window	聊天opener	聊天window
//
// ========	==========================	===========================
//
//   nwjs  	  global  	Window对象	Window对象	Window对象
//
// electron	   null   	  global  	  global  	Window对象
//
/////////////////////////////////////////////////////////////////////////////

label_window:
if (opener == null || (typeof nw === 'object' && opener == global)) {

    // 这是主窗口

    if (false) {
	// 这段浏览器代码工作正常, 但与 node_modules/iconv-lite 的功能重复,
	// 而且浏览器代码缺少 iconv.decodeStream() 功能, 因此废弃掉
	let tmpscript = window.document.createElement ('script');
	//script1.async='false';
	tmpscript.src = 'js/iconv-lite.js';
	window.document.head.appendChild(tmpscript);
    } else {
	// 本应在此引入 iconv, 但却引起 udp_socket.bind() 失败!!!! 把这句挪到
	// bind_OK_init() 中(延迟执行), 成功躲过了与 udp_socket.bind() 的冲突
	// iconv = require('./node_modules/iconv-lite');
    }

    //tmpscript = window.document.createElement ('script');
    ////script1.async='false';
    //tmpscript.src = 'js/sorttable.js';
    //window.document.head.appendChild(tmpscript);

    //tmpscript = window.document.createElement ('script');
    ////script1.async='false';
    //tmpscript.src = 'js/jschardet.min.js';
    //window.document.head.appendChild(tmpscript);

    window.addEventListener('online', function() {
	fail('网络已连, onLine=' + navigator.onLine +
		(navigator.connection ? ', 类型: ' +
			navigator.connection.type + ', 有效类型: ' +
			navigator.connection.effectiveType
			: ''));
	if (refresh_timeoutID)
	    clearTimeout(refresh_timeoutID);
	refresh_timeoutID = setTimeout(refresh, 60000);
    });

    window.addEventListener('offline', function() {
	fail('网络已断, onLine=' + navigator.onLine +
		(navigator.connection ? ', 类型: ' +
			navigator.connection.type + ', 有效类型: ' +
			navigator.connection.effectiveType
			: ''));
	if (refresh_timeoutID)
	    clearTimeout(refresh_timeoutID);
	refresh_timeoutID = setTimeout(refresh, 60000);
    });

    if (navigator.connection) {
	//navigator.connection.addEventListener('change', function () {
	//    fail('网络状态已变, 类型: ' + navigator.connection.type +
	//		', 有效类型: ' + navigator.connection.effectiveType);
	//    refresh ();
	//});
	navigator.connection.onchange = function () {
	    fail('网络状态已变, 类型: ' + navigator.connection.type +
			', 有效类型: ' + navigator.connection.effectiveType);
	    if (refresh_timeoutID)
		clearTimeout(refresh_timeoutID);
	    refresh_timeoutID = setTimeout(refresh, 60000);
	};
    }

    if (typeof nw === 'object') {	// nwjs 的情况
	let close_handler = function (e) {
	    if (! on_quit_app ()) {
		setTimeout( () => {
			thiswin.once('close', close_handler);
		}, 0);
		return;
	    }
	    quit_ongoing = 1;
	    updateMyNetInfo ();
	    setTimeout(updateMyNetInfo, 200);
	    setTimeout(updateMyNetInfo, 400);
	    setTimeout(updateMyNetInfo, 600);
	    if (! ADDRs || ! iconv ||
			ADDRs.replace(/192\.168\.\d+\.\d+,{0,1}/g, '')) {
		quit_app ();
	    }
	    setTimeout (() => {
		if (quit_ongoing <= 1) // 不支持 UPnP
		    quit_app ();
	    }, 1000);
	    setTimeout (() => {
	            quit_app (true);
	    }, 10000);  // 10 秒后强制退出
	};
	thiswin.once('close', close_handler);
    } else {				// electron 的情况
	require('electron').ipcRenderer.on('close-app', (event, args) => {
	    if (! on_quit_app ()) return;
	    quit_ongoing = 1;
	    updateMyNetInfo ();
	    setTimeout(updateMyNetInfo, 200);
	    setTimeout(updateMyNetInfo, 400);
	    setTimeout(updateMyNetInfo, 600);
	    if (! ADDRs || ! iconv ||
			ADDRs.replace(/192\.168\.\d+\.\d+,{0,1}/g, '')) {
		quit_app ();
	    }
	    setTimeout (() => {
		if (quit_ongoing <= 1) // 不支持 UPnP
		    quit_app ();
	    }, 1000);
	    setTimeout (() => {
	            quit_app (true);
	    }, 10000);  // 10 秒后强制退出
	});

	// 改用 IPC 方式后, 聊天窗的 close 事件处理也放在这里
	require('electron').ipcRenderer.on('close-guest', (event, id) => {
	    if (typeof Pal[id] !== 'object') {
		alert('严重错误! 用户 ' + id + ' 不存在, 无法关闭其聊天窗!');
		return;
	    }
	    if (typeof Pal[id].chatwin !== 'object') {
		alert('严重错误! 用户 ' + id + ' 的聊天窗丢失, 无法关闭它!');
		return;
	    }
	    if (! Pal[id].chatwin.confirm
		    ('若有文件正在传输, 关闭聊天窗将使传输中断。' +
		     '\r\n\r\n确实要关闭与 ' + Pal[id].nick +
		     ' 的对话?'))
		return;
	    if (Pal[id].chatwin) {
		Pal[id].chatwin = 0;
		updateTable(id);
	    }
	    require('electron').ipcRenderer.send('quit-guest', id);
	});
    }

    //updateMyNetInfo ();

    if (stored_nickname) {
	me.nick = stored_nickname.replace(/[\u0000-,/:-@[-`{|}\u007f]/g, '_');
    }

    if (stored_workgroup) {
	WGROUP = stored_workgroup.replace(/[\u0000-,/:-@[-`{|}\u007f]/g, '_');
    }

    if (! stored_randomID) {
	let a = new Uint32Array(2);
	window.crypto.getRandomValues(a);

	//stored_randomID = a[0].toString(16).padStart(8,'0') +
	//		    a[1].toString(16).padStart(8,'0');
	stored_randomID = ('00000000'+a[0].toString(16)).slice(-8) +
			  ('00000000'+a[1].toString(16)).slice(-8);
	stored_randomID = stored_randomID.toLowerCase();
	miaoconfig.randomID = stored_randomID;
	miaoconfig_needupdate = true;
	update_miaoconfig();
    }

    if (! miaoconfig.keyGenTime) {

	window.crypto.subtle.generateKey (
        {
	    //name: 'ECDSA',
	    //namedCurve: 'P-384'
            name: 'RSA-OAEP',
            modulusLength: 4096, //can be 1024, 2048, or 4096
            publicExponent: new Uint8Array ([0x01, 0x00, 0x01]),
            hash: {
		name: 'SHA-256'	// can be 'SHA-1', 'SHA-256', 'SHA-384',
				// or     'SHA-512'
	    },
        },
        true, //whether the key is extractable (i.e. can be used in exportKey)
        ['encrypt', 'decrypt']	// must be ['encrypt', 'decrypt']
				// or      ['wrapKey', 'unwrapKey']
	)
        .then (function (key) {
            //returns a keypair object
            window.crypto.subtle.exportKey (
                'jwk',		// can be 'jwk' (public or private),
				//	  'spki' (public only),
				// or     'pkcs8' (private only)
                key.privateKey	// can be a publicKey or privateKey,
				// as long as extractable was true
            )
            .then (function (keydata) {
		    privateKey = keydata;
		    const keystr = JSON.stringify (keydata);
		    miaoconfig.privateKey = keystr;
		    if (! miaoconfig.keyGenTime)
			miaoconfig.keyGenTime = Date.now();
		    miaoconfig_needupdate = true;
		    update_miaoconfig();
            })
            .catch (function (err) {
		setTimeout (() => {
		    if (miaoconfig.keyGenTime) {
			delete miaoconfig.keyGenTime;
			miaoconfig_needupdate = true;
			update_miaoconfig();
		    }
		    alert ('export private key error\n' + err);
		}, 500);
            });

            window.crypto.subtle.exportKey (
                'jwk',		// can be 'jwk' (public or private),
				//        'spki' (public only),
				// or     'pkcs8' (private only)
                key.publicKey	// can be a publicKey or privateKey,
				// as long as extractable was true
            )
            .then (function (keydata) {
		    publicKey = keydata;
		    const keystr = JSON.stringify (keydata);
		    miaoconfig.publicKey = keystr;
		    if (! miaoconfig.keyGenTime)
			miaoconfig.keyGenTime = Date.now();
		    miaoconfig_needupdate = true;
		    update_miaoconfig();
            })
            .catch (function (err) {
		setTimeout (() => {
		    if (miaoconfig.keyGenTime) {
			delete miaoconfig.keyGenTime;
			miaoconfig_needupdate = true;
			update_miaoconfig();
		    }
		    alert ('export public key error\n' + err);
		}, 500);
            });

        })
        .catch (function (err) {
            alert ('generateKey error\n' + err);
        });
    } else {

	window.crypto.subtle.importKey (
		'jwk',
		JSON.parse(miaoconfig.privateKey),
		{ name: 'RSA-OAEP', hash: 'SHA-256' },
		true,
		['decrypt']
	)
	.then (function (keydata) {
		privateKey = keydata;
	})
        .catch (function (err) {
            alert ('import private key error\n' + err);
        });

	window.crypto.subtle.importKey (
		'jwk',
		JSON.parse(miaoconfig.publicKey),
		{ name: 'RSA-OAEP', hash: 'SHA-256' },
		true,
		['encrypt']
	)
	.then (function (keydata) {
		publicKey = keydata;
	})
        .catch (function (err) {
            alert ('import public key error\n' + err);
        });
    }

    var tmp;
    tmp = window.document.createElement ('input');
    tmp.type = 'text';
    tmp.value = '255.255.255.255';
    tmp.id = 'detectip';
    tmp.placeholder = 'ip或域名';
    tmp.title = '填写您临时想要探测的地址，可以是任意一个 IP 地址或广播地址。'
		+ '255.255.255.255 只向本局域网探测，就是说，如果您是处于'
		+ '路由器内网，则不会把广播包发送到外网。您可以在这里填写'
		+ '想要探测的外网 IP。您也可以点击“更改设置”按钮并在' +
		'“临时探测”按钮左侧的框中填写想要探测的网段, 那更方便。\n' +
		'此处的 ip 使用完毕, 应该恢复为其默认值 255.255.255.255, ' +
		'或清空也行，否则, 作为调试输出的一个手段，程序会一直显示' +
		'来自此 ip 的消息。';
    window.document.body.appendChild(tmp);
    tmp = window.document.createElement ('input');
    tmp.type = 'text';
    tmp.value = '2425';
    tmp.id = 'detectport';
    tmp.placeholder = '端口(2425)';
    tmp.size = '10';
    tmp.title = '填写您想要探测的端口，通常就是 2425, 无需更改。\n\n' +
		'如果清空此处的值, 则在调试输出来自左侧 ip 的消息时，' +
		'会显示出完整的十六进制结果。';
    window.document.body.appendChild (tmp);
    tmp = window.document.createElement ('input');
    tmp.type = 'button';
    tmp.value = '探测';
    tmp.title = '立即向左侧设定的 IP 地址和端口发送探测数据包';
    tmp.onclick = function () { detect (); };
    window.document.body.appendChild(tmp);

    tmp = window.document.createElement ('input');
    tmp.type = 'button';
    tmp.value = '更改设置';
    tmp.title = '点击此按钮可显示或隐藏 miao 的设置';
    tmp.onclick = function () {
	if (this.value == '更改设置') {
	    span_settings.style.display = 'inline';
	    this.value = '隐藏设置';
	} else if (this.value == '隐藏设置') {
	    span_settings.style.display = 'none';
	    this.value = '更改设置';
	}
    };
    window.document.body.appendChild (tmp);

    const span_settings = window.document.createElement ('span');
    span_settings.id = 'settings';
    span_settings.style.display = 'none';
    window.document.body.appendChild (span_settings);

    span_settings.insertAdjacentHTML ('beforeEnd', '<br/>命令号: ');
    tmp = window.document.createElement ('input');
    tmp.type = 'text';
    tmp.id = 'command_number';
    tmp.placeholder = '整数';
    tmp.size = '10';
    tmp.title = '可用 0x 前缀来表示十六进制。\n\n' +
		'默认值是 0x20, 表示发送消息, 消息内容是右侧填写的字符串。';
    span_settings.appendChild (tmp);

    span_settings.insertAdjacentHTML ('beforeEnd', ' 字符串: ');
    tmp = window.document.createElement ('input');
    tmp.type = 'text';
    tmp.id = 'string_to_send';
    tmp.placeholder = '命令号所需的字符串';
    tmp.size = '20';
    tmp.title = '支持反斜杠转义, 注意:\n' +
		'回车、换行等不可打印字符, 必须使用其转义形式。\n' +
		'支持用 \\uXXXX （X是十六进制字符）来表示 Unicode 字符。\n' +
		'单引号不可以转义, 即不允许在其左边插入一个反斜杠。\n' +
		'双引号必须转义, 即必须在其左边插入一个反斜杠。\n' +
		'反斜杠必须转义, 即, 须敲入两个反斜杠来表示发送一个反斜杠。';
    span_settings.appendChild (tmp);

    span_settings.insertAdjacentHTML ('beforeEnd', ' charset: ');
    tmp = window.document.createElement ('input');
    tmp.type = 'text';
    tmp.id = 'charset_to_use';
    tmp.placeholder = 'utf8';
    tmp.size = '8';
    tmp.title = '填写接收者所用的字符集编码, 默认值是 utf8。\n\n' +
		'如果对方使用的是飞秋(feiq)，请填写 gb18030。';
    span_settings.appendChild (tmp);

    tmp = window.document.createElement ('input');
    tmp.type = 'button';
    tmp.value = '立即发送';
    tmp.title = '向上述 IP 地址和端口发送命令号以及字符串；请勿滥用此功能！';
    tmp.onclick = function () {
	who.IP = '255.255.255.255';
	who.port = 2425;

	let detip = window.document.getElementById('detectip').value;
	let detport = window.document.getElementById('detectport').value;
	let cmd = window.document.getElementById('command_number').value;
	let str = window.document.getElementById('string_to_send').value;
	let charset = window.document.getElementById('charset_to_use').value;
	    charset = charset.toLowerCase();

	str = '"' + str + '"'; // 两边加上双引号
	try {
	    str = JSON.parse(str);
	} catch (e) {
	    alert ('字符串解析出错，请修正后再发送。\n' +
			'错误通常是由不正确的引号或反斜杠转义引起的。\n' +
			'出错的字符串是:\n' +
			JSON.stringify(str) +
			'\n出错信息是:\n' + e.message);
	    return;
	}
	if (charset != '' && charset != 'utf8' && charset != 'gb18030') {
	    alert ('字符集编码应该是 utf8 或 gb18030，注意不要敲入空格。');
	    return;
	}
	if (detip == '' || detip == '255.255.255.255') {
	    alert ('您没有填写有效 ip，不能发送。');
	    return;
	}
	who.IP = detip;
	if (cmd == '')
	    cmd = '0x20'; // 发消息
	cmd = Number(cmd);
	if (! Number.isInteger(cmd)) {
	    alert ('您没有填写有效命令号，不能发送。');
	    return;
	}

	if (detport != '') {
	    detport = Number(detport);
	    if (! Number.isInteger(detport)) {
		alert ('您没有填写有效端口号，不能发送。');
		return;
	    }
	    who.port = detport;
	}
	who.charset = charset;
	sendMsg (me, who, cmd, str);
	who.charset = '';
	who.IP = '255.255.255.255';
	who.port = 2425;
    };
    span_settings.appendChild (tmp);

    span_settings.insertAdjacentHTML ('beforeEnd', '<br/>昵称: ');
    tmp = window.document.createElement ('input');
    tmp.type = 'text';
    if (! stored_nickname) {
	tmp.value = '';
    } else {
	tmp.value = stored_nickname
			.replace(/[\u0000-,/:-@[-`{|}\u007f]/g, '_');
    }
    tmp.id = 'nickname';
    tmp.placeholder = '姓名';
    tmp.size = '20';
    tmp.title = '不要太长，不要包含冒号、引号等字符';
    tmp.onchange = function() {
	me.nick = this.value
			.replace(/[\u0000-,/:-@[-`{|}\u007f]/g, '_');
	miaoconfig.nickname = me.nick;
	miaoconfig_needupdate = true;
	update_miaoconfig();
    };
    span_settings.appendChild (tmp);

    span_settings.insertAdjacentHTML ('beforeEnd', ' 工作组: ');
    tmp = window.document.createElement ('input');
    tmp.type = 'text';
    if (! stored_workgroup) {
	tmp.value = '';
    } else {
	tmp.value = stored_workgroup;
    }
    tmp.id = 'workgroup';
    tmp.placeholder = '工作组名';
    tmp.size = '30';
    tmp.title = '不要太长，不要包含冒号、引号等字符';
    tmp.onchange = function() {
	WGROUP = this.value;
	miaoconfig.workgroup = this.value;
	miaoconfig_needupdate = true;
	update_miaoconfig();
    };
    span_settings.appendChild(tmp);

    span_settings.insertAdjacentHTML ('beforeEnd', '<br/>Ethereal 账户: ');
    tmp = window.document.createElement ('input');
    tmp.type = 'text';
    if (! stored_emailuser) {
	tmp.value = '';
    } else {
	tmp.value = stored_emailuser;
    }
    tmp.id = 'emailuser';
    tmp.placeholder = ' Ethereal 邮箱地址';
    tmp.size = '50';
    tmp.title = '高级用户才需要，普通用户无需填写';
    tmp.onchange = function() {
	stored_emailuser = this.value;
	miaoconfig.emailuser = this.value;
	miaoconfig_needupdate = true;
	update_miaoconfig();
	if (stored_mailuser && stored_mailpass) {
	    effective_mailuser = stored_mailuser;
	    effective_mailpass = stored_mailpass;
	} else if (stored_emailuser && stored_emailpass) {
	    effective_mailuser = stored_emailuser;
	    effective_mailpass = stored_emailpass;
	} else {
	    effective_mailuser = undefined;
	    effective_mailpass = undefined;
	}
    };
    span_settings.appendChild (tmp);

    span_settings.insertAdjacentHTML ('beforeEnd', '<br/>Ethereal 口令: ');
    tmp = window.document.createElement ('input');
    tmp.type = 'text';
    if (! stored_emailpass) {
	tmp.value = '';
    } else {
	tmp.value = stored_emailpass;
    }
    tmp.id = 'emailpass';
    tmp.placeholder = 'Ethereal 邮箱口令';
    tmp.size = '40';
    tmp.title = '高级用户才需要，普通用户无需填写';
    tmp.onchange = function() {
	stored_emailpass = this.value;
	miaoconfig.emailpass = this.value;
	miaoconfig_needupdate = true;
	update_miaoconfig();
	if (stored_mailuser && stored_mailpass) {
	    effective_mailuser = stored_mailuser;
	    effective_mailpass = stored_mailpass;
	} else if (stored_emailuser && stored_emailpass) {
	    effective_mailuser = stored_emailuser;
	    effective_mailpass = stored_emailpass;
	} else {
	    effective_mailuser = undefined;
	    effective_mailpass = undefined;
	}
    };
    span_settings.appendChild(tmp);
    tmp = window.document.createElement ('input');
    tmp.type = 'button';
    tmp.value = '申请账户';
    tmp.id = 'getEmailAccount';
    tmp.title = '高级用户才需要，普通用户请勿申请邮箱账户';
    tmp.onclick = function() {
	if (stored_emailuser || stored_emailpass)
	{
	    alert ('您已有 ethereal 账户和/或口令。您必须先把旧的账户和' +
		   '口令都清空，\n才能申请新账户。在清空之前，建议您手动' +
		   '备份旧的账户和口令。\n本程序不会自动为您备份。请注意' +
		   '不要浪费 ethereal 账户资源。\n如果由于某种原因使得' +
		   '现有账户无法工作，您可以申请一个新账户。\n但是，一段' +
		   '时间（如一个星期）之后，旧账户可能恢复正常，此时，\n' +
		   '旧账户可用于您的另一台设备。');
	    return;
	}
	if (! confirm ( '注意节约邮箱资源，仅在必要时申请邮箱。\n' +
			'您确定现在申请 ethereal 邮箱吗？'))
	    return;
	if (! navigator.onLine)
	{
	    alert ('您没有连网，无法申请 ethereal 邮箱。');
	    return;
	}
	const nodemailer = require('./nodemailer/lib/nodemailer');
	nodemailer.createTestAccount((err, account) => {
	    if (err) {
		alert ('网络可能受限，申请 ethereal 邮箱失败: ' + err);
		return;
	    }
	    stored_emailuser = account.user;
	    stored_emailpass = account.pass;
	    if (stored_mailuser && stored_mailpass) {
		effective_mailuser = stored_mailuser;
		effective_mailpass = stored_mailpass;
	    } else if (stored_emailuser && stored_emailpass) {
		effective_mailuser = stored_emailuser;
		effective_mailpass = stored_emailpass;
	    } else {
		effective_mailuser = undefined;
		effective_mailpass = undefined;
	    }
	    window.document.getElementById ('emailuser').value =
			stored_emailuser;
	    window.document.getElementById ('emailpass').value =
			stored_emailpass;
	    miaoconfig.emailuser = stored_emailuser;
	    miaoconfig.emailpass = stored_emailpass;
	    miaoconfig_needupdate = true;
	    update_miaoconfig();
	    alert ('邮箱申请成功，已为您保存账户和口令。' +
		   '建议备份到别处，以免丢失。');
	});
    };
    span_settings.appendChild (tmp);

    span_settings.insertAdjacentHTML ('beforeEnd', '<br/>邮箱: ');
    tmp = window.document.createElement ('input');
    tmp.type = 'text';
    if (! stored_mailuser) {
	tmp.value = '';
    } else {
	tmp.value = stored_mailuser;
    }
    tmp.id = 'mailuser';
    tmp.placeholder = '普通邮箱地址';
    tmp.size = '24';
    tmp.title = '填写您的普通邮箱地址, 最好是一个稳定可靠的 VIP 邮箱';
    tmp.onchange = function() {
	stored_mailuser = this.value;
	miaoconfig.mailuser = this.value;
	miaoconfig_needupdate = true;
	update_miaoconfig();
	if (stored_mailuser && stored_mailpass) {
	    effective_mailuser = stored_mailuser;
	    effective_mailpass = stored_mailpass;
	} else if (stored_emailuser && stored_emailpass) {
	    effective_mailuser = stored_emailuser;
	    effective_mailpass = stored_emailpass;
	} else {
	    effective_mailuser = undefined;
	    effective_mailpass = undefined;
	}
    };
    span_settings.appendChild (tmp);

    span_settings.insertAdjacentHTML ('beforeEnd', ' 口令: ');
    tmp = window.document.createElement ('input');
    tmp.type = 'text';
    if (! stored_mailpass) {
	tmp.value = '';
    } else {
	tmp.value = '*'.repeat(Number(stored_mailpass.length));
    }
    tmp.id = 'mailpass';
    tmp.placeholder = '普通邮箱口令';
    tmp.size = '16';
    tmp.title = '填写访问您的普通邮箱所需的口令';
    tmp.onchange = function() {
	stored_mailpass = this.value;
	miaoconfig.mailpass = this.value;
	miaoconfig_needupdate = true;
	update_miaoconfig();
	this.value = '*'.repeat(this.value.length);
	if (stored_mailuser && stored_mailpass) {
	    effective_mailuser = stored_mailuser;
	    effective_mailpass = stored_mailpass;
	} else if (stored_emailuser && stored_emailpass) {
	    effective_mailuser = stored_emailuser;
	    effective_mailpass = stored_emailpass;
	} else {
	    effective_mailuser = undefined;
	    effective_mailpass = undefined;
	}
    };
    span_settings.appendChild(tmp);

    span_settings.insertAdjacentHTML ('beforeEnd', ' 授权: ');
    tmp = window.document.createElement ('input');
    tmp.type = 'text';
    if (! stored_smtppass) {
	tmp.value = '';
    } else {
	tmp.value = '*'.repeat(Number(stored_smtppass.length));
    }
    tmp.id = 'smtppass';
    tmp.placeholder = '发件授权码';
    tmp.size = '16';
    tmp.title = '通常与邮箱口令相同, 无需填写。\r\n\r\n' +
		'否则请从邮箱的服务商那里获取授权码。';
    tmp.onchange = function() {
	stored_smtppass = this.value;
	miaoconfig.smtppass = this.value;
	miaoconfig_needupdate = true;
	update_miaoconfig();
	this.value = '*'.repeat(this.value.length);
    };
    span_settings.appendChild(tmp);

    span_settings.insertAdjacentHTML ('beforeEnd', '<br/>IMAP: ');
    tmp = window.document.createElement ('input');
    tmp.type = 'text';
    if (! stored_imaphost) {
	tmp.value = '';
    } else {
	tmp.value = stored_imaphost;
    }
    tmp.id = 'imaphost';
    tmp.placeholder = 'IMAP 收件主机';
    tmp.size = '20';
    tmp.title = '通常是 imap.*.com';
    tmp.onchange = function() {
	stored_imaphost = this.value;
	miaoconfig.imaphost = this.value;
	miaoconfig_needupdate = true;
	update_miaoconfig();
    };
    span_settings.appendChild (tmp);

    span_settings.insertAdjacentHTML ('beforeEnd', ' 端口: ');
    tmp = window.document.createElement ('input');
    tmp.type = 'text';
    if (! stored_imapport) {
	tmp.value = 993;
	stored_imapport = 993;
    } else {
	tmp.value = stored_imapport;
    }
    tmp.id = 'imapport';
    tmp.placeholder = '(993)';
    tmp.size = '6';
    tmp.title = '通常是 993';
    tmp.onchange = function() {
	if (! tmp.value)
	    tmp.value = 993;
	stored_imapport = this.value;
	miaoconfig.imapport = this.value;
    };
    span_settings.appendChild (tmp);

    imap_secure = window.document.createElement('input');
    imap_secure.type = 'checkbox';
    imap_secure.title = '通常启用安全协议';
    //imap_secure.style.display = 'inline-block';

    if (! stored_imapsecure) {
	imap_secure.checked = true;
    } else {
	imap_secure.checked = (stored_imapsecure != 0);
    }
    imap_secure.onclick = function () {
	if (imap_secure.checked)
	    delete miaoconfig.imapsecure;
	    //localStorage.setItem ('imapsecure', '1');
	else
	    miaoconfig.imapsecure = '0';
	    //localStorage.removeItem ('imapsecure');
	miaoconfig_needupdate = true;
	update_miaoconfig();
    };

    span_settings.appendChild(imap_secure);

    span_settings.insertAdjacentHTML ('beforeEnd', '安全 ');

    imap_rejectunauth = window.document.createElement('input');
    imap_rejectunauth.type = 'checkbox';
    imap_rejectunauth.title = '通常不拒绝, 表示允许自签证书';
    //imap_rejectunauth.style.display = 'inline-block';

    if (! stored_imaprejectunauth) {
	imap_rejectunauth.checked = false;
    } else {
	imap_rejectunauth.checked = true;
    }
    imap_rejectunauth.onclick = function () {
	if (imap_rejectunauth.checked)
	    miaoconfig.imaprejectunauth = '1';
	else
	    delete miaoconfig.imaprejectunauth;
	miaoconfig_needupdate = true;
	update_miaoconfig();
    };

    span_settings.appendChild(imap_rejectunauth);

    span_settings.insertAdjacentHTML ('beforeEnd', '拒绝无效证书');

    span_settings.insertAdjacentHTML ('beforeEnd', '<br/>SMTP: ');
    tmp = window.document.createElement ('input');
    tmp.type = 'text';
    if (! stored_smtphost) {
	tmp.value = '';
    } else {
	tmp.value = stored_smtphost;
    }
    tmp.id = 'smtphost';
    tmp.placeholder = 'SMTP 发件主机';
    tmp.size = '20';
    tmp.title = '通常是 smtp.*.com';
    tmp.onchange = function() {
	stored_smtphost = this.value;
	miaoconfig.smtphost = this.value;
	miaoconfig_needupdate = true;
	update_miaoconfig();
    };
    span_settings.appendChild (tmp);

    span_settings.insertAdjacentHTML ('beforeEnd', ' 端口: ');
    tmp = window.document.createElement ('input');
    tmp.type = 'text';
    if (! stored_smtpport) {
	tmp.value = 465;
	stored_smtpport = 465;
    } else {
	tmp.value = stored_smtpport;
    }
    tmp.id = 'smtpport';
    tmp.placeholder = '(465)';
    tmp.size = '6';
    tmp.title = '端口 465 是加密通讯，其它端口（如587, 25）不使用加密';
    tmp.onchange = function() {
	if (! tmp.value)
	    tmp.value = 465;
	stored_smtpport = this.value;
	miaoconfig.smtpport = this.value;
	miaoconfig_needupdate = true;
	update_miaoconfig();
    };
    span_settings.appendChild (tmp);

    smtp_secure = window.document.createElement('input');
    smtp_secure.type = 'checkbox';
    smtp_secure.title = '端口 465 启用安全协议, 其它端口(如587, 25)不启用';
    //smtp_secure.style.display = 'inline-block';

    if (! stored_smtpsecure) {
	smtp_secure.checked = true;
    } else {
	smtp_secure.checked = (stored_smtpsecure != 0);
    }
    smtp_secure.onclick = function () {
	if (smtp_secure.checked)
	    delete miaoconfig.smtpsecure;
	    //localStorage.setItem ('smtpsecure', '1');
	else
	    miaoconfig.smtpsecure = '0';
	    //localStorage.removeItem ('smtpsecure');
	miaoconfig_needupdate = true;
	update_miaoconfig();
    };

    span_settings.appendChild(smtp_secure);

    span_settings.insertAdjacentHTML ('beforeEnd', '安全 ');

    smtp_rejectunauth = window.document.createElement('input');
    smtp_rejectunauth.type = 'checkbox';
    smtp_rejectunauth.title = '通常不拒绝, 表示允许自签证书';
    //smtp_rejectunauth.style.display = 'inline-block';

    if (! stored_smtprejectunauth) {
	smtp_rejectunauth.checked = false;
    } else {
	smtp_rejectunauth.checked = true;
    }
    smtp_rejectunauth.onclick = function () {
	if (smtp_rejectunauth.checked)
	    miaoconfig.smtprejectunauth = '1';
	else
	    delete miaoconfig.smtprejectunauth;
	miaoconfig_needupdate = true;
	update_miaoconfig();
    };

    span_settings.appendChild(smtp_rejectunauth);

    span_settings.insertAdjacentHTML ('beforeEnd', '拒绝无效证书');

//    const span_passwarn = window.document.createElement ('span');
    const span_execwarn = window.document.createElement ('span');

    span_settings.insertAdjacentHTML ('beforeEnd', '<br/>Rexec 静态口令: ');
    tmp = window.document.createElement ('input');
    tmp.type = 'text';
    //tmp.pattern = '\w*';
    if (! stored_rexecStaticPass) {
	tmp.value = '';
    } else {
	tmp.value = stored_rexecStaticPass
	    .replace(/[\s\0-/:-@[-^\x60\x7b-\x7f]/g, '');
    }
    tmp.id = 'rexecStaticPass';
    tmp.placeholder = '填写远程执行本机命令所需的静态口令';
    tmp.size = '16';
    tmp.title = '高级用户才需要，普通用户无需填写。\n\n' +
		'口令为空，表示禁止远程访问。\n\n' +
		'口令为字符串 "null"，也表示禁止远程访问。\n\n' +
		'口令应为字母、汉字、数字、下划线的组合。\n\n' +
		'口令不可以包含其他字符（例如英文标点符号等）。\n\n' +
		'口令中的非法字符会被自动过滤掉。\n\n' +
		'注意：任何一个能使用这台电脑的人, 都能修改这里设置的口令！' +
		'\n\n或者未经您同意而随意添加口令！\n\n' +
		'口令使用完毕，要及时清空！谨防他人用此口令远程控制此电脑！';
    tmp.onchange = function() {
	this.value = this.value
	    .replace(/[\s\0-/:-@[-^\x60\x7b-\x7f]/g, '');
	stored_rexecStaticPass = this.value;
	miaoconfig.rexecStaticPass = this.value;
	miaoconfig_needupdate = true;
	update_miaoconfig();
//	span_passwarn.innerText = JSON.stringify(this.value);
        span_execwarn.innerText = crypto.createHash('md5').update(this.value)
				.digest('base64');
	if (! this.value) {
//	    span_passwarn.style.display = 'none';
	    span_execwarn.style.display = 'none';
	} else {
//	    span_passwarn.style.display = 'inline';
	    span_execwarn.style.display = 'inline';
	}
	this.value = '*'.repeat(this.value.length);
    };
    span_settings.appendChild (tmp);

//    span_passwarn.id = 'passwarn';
//    span_passwarn.title = 'Rexec 口令非空警告！请确认口令是您本人设置的！';
//    span_passwarn.style.display = 'none';
//    span_passwarn.style.color = 'red';
//    span_settings.appendChild (span_passwarn);

//    span_passwarn.innerText = JSON.stringify(tmp.value);
    span_execwarn.innerText = crypto.createHash('md5').update(tmp.value)
    				.digest('base64');

    span_execwarn.id = 'execwarn';
    span_execwarn.title = 'Rexec 口令非空警告 ！安全起见请在设置中清空口令！';
    span_execwarn.style.display = 'none';
    span_execwarn.style.color = 'red';

    if (! tmp.value) {
//	    span_passwarn.style.display = 'none';
	    span_execwarn.style.display = 'none';
    } else {
//	    span_passwarn.style.display = 'inline';
	    span_execwarn.style.display = 'inline';
    }
    tmp.value = '*'.repeat(Number(tmp.value.length));

    maildebug = window.document.createElement('input');
    maildebug.type = 'checkbox';
    maildebug.id = 'maildebug';
    maildebug.title = '将邮件原始数据放在 mailbox 文件夹下，' +
			'而不是在聊天窗中显示邮件内容';

    maildebug.onclick = function () {
	if (recvMail) {
	    recvMail.send ({
		maildebug: this.checked,
		run: 'update.maildebug'
	    });
	}
    };

    span_settings.appendChild(maildebug);

    span_settings.insertAdjacentHTML ('beforeEnd', '调试邮件　');

    span_settings.insertAdjacentHTML ('beforeEnd', '　邮件起始UID: ');
    tmp = window.document.createElement ('input');
    tmp.type = 'text';
    if (! stored_mailStartUID) {
	tmp.value = 0;
    } else {
	tmp.value = stored_mailStartUID;
    }
    tmp.id = 'mailStartUID';
    tmp.placeholder = '整数(0)';
    tmp.size = '8';
    tmp.title = '从这个 UID 开始抓取邮件';
    tmp.onchange = function() {
	stored_mailStartUID = this.value;
	miaoconfig.mailStartUID = this.value;
	miaoconfig_needupdate = true;
	update_miaoconfig();
	if (recvMail) {
	    recvMail.send ({
		uid: stored_mailStartUID,
		run: 'update.uid'
	    });
	}
    };
    span_settings.appendChild (tmp);

    span_settings.insertAdjacentHTML ('beforeEnd',
		'<br/>启动后首次抓取邮件的等待时间（秒）: ');
    tmp = window.document.createElement ('input');
    tmp.type = 'text';
    if (miaoconfig.startupMailFetchWait == undefined ||
	miaoconfig.startupMailFetchWait == null ||
	miaoconfig.startupMailFetchWait == '' ||
	isNaN(miaoconfig.startupMailFetchWait)) {
	tmp.value = 60;
	miaoconfig.startupMailFetchWait = 60;
    } else {
	tmp.value = miaoconfig.startupMailFetchWait;
    }
    tmp.id = 'startupMailFetchWait';
    tmp.placeholder = '默认值(60)';
    tmp.size = '8';
    tmp.title = '0 表示启动后不自动抓取邮件。' +
		'正数表示启动后抓取邮件需要等待的秒数。';
    tmp.onchange = function() {
	if (miaoconfig.startupMailFetchWait != this.value) {
	    if (isNaN(this.value))
		miaoconfig.startupMailFetchWait = 60;
	    else
		miaoconfig.startupMailFetchWait = this.value;
	    miaoconfig_needupdate = true;
	    update_miaoconfig();
	}
    };
    span_settings.appendChild (tmp);

    span_settings.insertAdjacentHTML ('beforeEnd', '<br/>好友设置，每行写一条'
	+ '信息: 好友邮箱、机器码、昵称、工作组(用空格分隔):<br/>');

    tmp = window.document.createElement ('textarea');
    tmp.value = stored_friends;
    tmp.id = 'friends';
    tmp.rows = 4;
    tmp.cols = 100;
    tmp.title = '高级用户才需要，普通用户无需填写';
    //tmp.maxlength = 2000;
    //tmp.minlength = 1; // minlength is not HTML5
    tmp.onchange = function() {
	this.value = this.value.replace(/^\s+/, '').replace(/\n\s+/g, '\n');
	stored_friends = this.value;
	miaoconfig.friends = this.value;
	miaoconfig_needupdate = true;
	update_miaoconfig();
	//updateTable(); // update_friends() 自动更新整个表格造成无限循环! 
    };
    tmp.placeholder = '每行写一条信息: 好友邮箱、机器码、昵称、工作组'
			+ '(用空格分隔)';
    span_settings.appendChild (tmp);

    span_settings.insertAdjacentHTML ('beforeEnd', '<br/>');

    auto_recvfiles = window.document.createElement('input');
    auto_recvfiles.type = 'checkbox';
    auto_recvfiles.title = '勾选后将自动接收任何人发来的文件\n' +
				'请谨慎勾选！';
    //auto_recvfiles.style.display = 'inline-block';

    if (! stored_autorecv) {
	auto_recvfiles.checked = false;
    } else {
	auto_recvfiles.checked = true;
    }
    auto_recvfiles.onclick = function () {
	if (auto_recvfiles.checked)
	    miaoconfig.autorecv = '1';
	else
	    delete miaoconfig.autorecv;
	miaoconfig_needupdate = true;
	update_miaoconfig();
    };

    span_settings.appendChild(auto_recvfiles);

    span_settings.insertAdjacentHTML ('beforeEnd', '全局自动接收文件 ');

//    enable_sync_directory = window.document.createElement('input');
//    enable_sync_directory.type = 'checkbox';
//    enable_sync_directory.title = '勾选或勾掉都将在下次启动时生效。\n' +
//	'同步文件夹名必须为 sync, 放在接收对方文件的目录下。';
//    //enable_sync_directory.style.display = 'inline-block';
//
//    if (stored_enableSyncDir + '' == 'null') {
//	enable_sync_directory.checked = false;
//    } else {
//	enable_sync_directory.checked = true;
//    }
//    enable_sync_directory.onclick = function () {
//	if (enable_sync_directory.checked)
//	    miaoconfig.enableSyncDir = '1';
//	else
//	    delete miaoconfig.enableSyncDir;
//    };
//
//    span_settings.appendChild(enable_sync_directory);
//
//    span_settings.insertAdjacentHTML ('beforeEnd', '启用同步文件夹');

    span_settings.insertAdjacentHTML ('beforeEnd', '<br/>本机服务器配置，'
	+ '每行写一条信息: 协议、端口号、路径(用空格分隔):<br/>');

    tmp = window.document.createElement ('textarea');
    if (! miaoconfig.services) {
	tmp.value = '';
	miaoconfig.services = '';
    } else {
	tmp.value = miaoconfig.services;
    }
    tmp.id = 'services';
    tmp.rows = 4;
    tmp.cols = 100;
    //tmp.maxlength = 2000;
    //tmp.minlength = 1; // minlength is not HTML5
    tmp.onchange = function() {
	miaoconfig.services = this.value;
	miaoconfig_needupdate = true;
	update_miaoconfig();
    };
    tmp.placeholder = '每行填写协议、端口、路径。后两者可省略。' +
			'但如果要填写路径，则端口也必须填写。';
    tmp.title = '这里填写的是操作系统层面的服务配置信息，目的是方便别人' +
		'访问咱的电脑。这些信息会向所有用户通告。由于服务器软件不是' +
		' miao 内置的，而是咱自己安装在操作系统中的，因此 miao 软件' +
		'无法保证这些服务器软件无漏洞、无后门。保证安全是咱自己的事' +
		'，咱自己应该使用安全可靠的服务器软件，严防黑客入侵。\n\n' +
		'空白行表示开始注释，空白行后面的行都是无效的，不再处理。';
    span_settings.appendChild (tmp);

    span_settings.insertAdjacentHTML ('beforeEnd', '<br/>群发设置，每行写一条'
	+ '信息: 接收者的昵称、工作组名、IP地址(三者用英文冒号分隔):<br/>');

    tmp = window.document.createElement ('textarea');
    if (! stored_cc) {
	tmp.value = '';
    } else {
	tmp.value = stored_cc;
    }
    tmp.id = 'cc';
    tmp.rows = 4;
    tmp.cols = 100;
    //tmp.maxlength = 2000;
    //tmp.minlength = 1; // minlength is not HTML5
    tmp.onchange = function() {
	stored_cc = this.value;
	miaoconfig.cc = this.value;
	miaoconfig_needupdate = true;
	update_miaoconfig();
    };
    tmp.placeholder = '每行填写接收者的昵称、工作组、IP地址, '
			+ '三者之间用英文冒号分隔, 冒号两边不可'
			+ '带有多余的空格。可以使用正则表达式。'
			+ '可以填写多行，每行指定符合某条件的用户群体。'
			+ '注意，每行指定的用户群体都会收到消息。';
    tmp.title = '这里填写的信息，仅当您点击“发送”按钮之前勾选了“群发”' +
		'时才起作用。\n每行指定符合条件的某一群体。各行指定的' +
		'不同群体都会收到消息。\n' +
		'填写完成之后，只需用鼠标在文本框外点一下就行了。\n' +
		'注意: 空白行之后的行都不起作用。\n' +
		'举例:\n1. 某行填写 “张三” 会将消息发给昵称或组名中含有' +
		'“张三”这个字符串的所有用户。填写“团支部”同理。\n' +
		'2. 某行填写“张三:团支部”会发给昵称以“张三”结尾且组名' +
		'以“团支部”开头的所有用户。\n' +
		'3. 某行填写“^张三:”会发给昵称精确地等于“张三”的所有用户。\n' +
		'4. 某行填写“:团支部:”会发给组名精确地等于“团支部”的所有' +
		'用户。\n' +
		'5. 某行填写“:192[.]168[.]1[.]103$”会发给IP地址精确地等于' +
		'“192.168.1.103”的所有用户。\n' +
		'6. 某行填写“^.*张三.*:.*团支部.*:192[.]168[.].*$”会发给' +
		'那些昵称中含有“张三”且组名中含有“团支部”且IP地址' +
		'以 192.168. 开头的所有用户。\n' +
		'7. 某行填写“.*”会发给当前用户列表中的所有用户。\n' +
		'请在 Internet 上查阅有关 JavaScript 正则表达式的资料。';
    span_settings.appendChild (tmp);

    span_settings.insertAdjacentHTML ('beforeEnd', '<br/>外网 IP: ');

    tmp = window.document.createElement ('input');
    tmp.type = 'text';
    if (! stored_outer_LAN_IP) {
	tmp.value = '';
    } else {
	tmp.value = stored_outer_LAN_IP;
    }
    tmp.id = 'outer_LAN_IP';
    tmp.placeholder = '无值(需要探测)';
    tmp.title = '这里放置探测到的路由器外网 IP。\n' +
		'若路由器支持 UPnP, 本软件会自动探测并填写路由器外网 IP。\n' +
		'若路由器不支持 UPnP, 您需要手动填写正确的外网 IP 值。\n' +
		'注意电脑可能会切换到某个不支持 UPnP 的新路由器（WiFi）上,\n' +
		'此时, 上次自动探测（或手动设定）的外网 IP 值还在, 但已\n' +
		'无效, 需要您为新路由器（WiFi）填写正确的外网 IP 值。\n\n' +
		'错误的外网 IP 值只是不起作用而已, 不会有其它严重问题。\n';
    tmp.onchange = function() {
	stored_outer_LAN_IP = this.value;
	miaoconfig.outer_LAN_IP = this.value;
	miaoconfig_needupdate = true;
	update_miaoconfig();
    };
    span_settings.appendChild (tmp);

    span_settings.insertAdjacentHTML ('beforeEnd', ' 外网掩码: ');

    tmp = window.document.createElement ('input');
    tmp.type = 'text';
    if (! stored_outer_LAN_mask) {
	tmp.value = 22;
	stored_outer_LAN_mask = 22;
    } else {
	tmp.value = stored_outer_LAN_mask;
    }
    tmp.id = 'outer_LAN_mask';
    tmp.placeholder = 'WAN 口子网掩码';
    tmp.title = '填写路由器外网（WAN 口）的子网掩码, 最小值 8, 最大值 32。\n' +
		'注意: 这里填写的值仅用于确定探测外网的范围（好友人数）;\n' +
		'这只是虚拟的子网掩码, 与路由器外网的真实子网掩码无关。填\n' +
		'写的值越小, 路由器外网 IP 的探测范围就越大。探测期间您可\n' +
		'能无法正常收发消息，因此探测范围不要太大。当探测范围较大\n' +
		'时, 请勿频繁点击“刷新好友”按钮, 以免软件超载崩溃。\n' +
		'填写 32 —— 探测 1 人（只探测自己）\n' +
		'填写 31 —— 探测 2 人（自己以及另外 1 人）\n' +
		'填写 30 —— 探测 4 人（自己以及另外 3 人）\n' +
		'填写 29 —— 探测 8 人\n' +
		'填写 28 —— 探测 16 人\n' +
		'填写 27 —— 探测 32 人\n' +
		'填写 26 —— 探测 64 人\n' +
		'填写 25 —— 探测 128 人, 完成探测约需 1 秒\n' +
		'填写 24 —— 探测 256 人, 完成探测约需 2 秒\n' +
		'填写 23 —— 探测 512 人, 完成探测约需 4 秒\n' +
		'默认(22)—— 探测 1024 人, 完成探测约需 8 秒\n' +
		'..................................................\n' +
		'填写 16 —— 探测 65536 人, 完成探测约需 8 分钟\n' +
		'填写 15 —— 探测 131072 人, 完成探测约需 16 分钟\n' +
		'填写 14 —— 探测 262144 人, 完成探测约需 32 分钟\n' +
		'填写 13 —— 探测 524288 人, 完成探测约需 64 分钟\n' +
		'..................................................\n' +
		'填写  8 —— 探测 16777216 人, 完成探测约需 34 小时';
    tmp.onchange = function() {
	if (! this.value)
	    this.value = 22;
	if (this.value < 8)
	    this.value = 8;
	stored_outer_LAN_mask = this.value;
	miaoconfig.outer_LAN_mask = this.value;
	miaoconfig_needupdate = true;
	update_miaoconfig();
    };
    span_settings.appendChild (tmp);

    span_settings.insertAdjacentHTML ('beforeEnd', '<br/>');

    tmp = window.document.createElement ('textarea');
    if (! stored_tmpSubnets) {
	tmp.value = '';
    } else {
	tmp.value = stored_tmpSubnets;
    }
    tmp.id = 'tmpSubnets';
    tmp.rows = 4;
    tmp.cols = 20;
    tmp.maxlength = 2000;
    tmp.minlength = 1;
    tmp.onchange = function() {
	stored_tmpSubnets = this.value;
	miaoconfig.tmpSubnets = this.value;
	miaoconfig_needupdate = true;
	update_miaoconfig();
    };
    tmp.placeholder = '填写临时探测网段(形如192.168.0.1/24), ' +
			'多个网段用回车分隔。';
    tmp.title = '这里填写的网段, 只在手动点击“临时探测”按钮时才起作用。\n' +
    		'您不想每次启动 miao 时自动刷新这些网段，因此，您在这里' +
		'填写网段。\n如果您想在 miao 启动时自动刷新网段，请在右侧' +
		'“刷新好友”按钮旁边的框里填写网段信息。\n此处要填写的网段' +
		'格式也与右侧框相同, 请把鼠标停靠在右侧框上阅读详细解释。';
    span_settings.appendChild (tmp);

    tmp = window.document.createElement ('input');
    tmp.type = 'button';
    tmp.value = '临时探测';
    tmp.title = '点击此按钮会立即刷新左侧框里填写的网段。' +
    		'如果您想刷新右侧框里的网段，请点击“刷新好友”按钮。';
    tmp.onclick = function() {
	// 向自定义网段发送试探消息。只能发纯 ascii 消息
	who.IP = '255.255.255.255';
	who.port = 2425;
	if (stored_tmpSubnets) {
	    var seg_v = stored_tmpSubnets.split (/[\s,;]+/);
	    for (let i in seg_v) {
		detectSubnets (seg_v[i], false);
	    }
	}
    };
    span_settings.appendChild (tmp);

    tmp = window.document.createElement ('textarea');
    if (! miaoconfig.palSubnets) {
	    tmp.value = '';
    } else {
	    tmp.value = miaoconfig.palSubnets;
    }
    tmp.id = 'palSubnets';
    tmp.rows = 4;
    tmp.cols = 20;
    tmp.maxlength = 2000;
    tmp.minlength = 1;
    tmp.onchange = function() {
	this.value = this.value.replace(/^[\s,;]+/,'').replace(/[\s,;]+$/,'');
	miaoconfig.palSubnets = this.value;
	miaoconfig_needupdate = true;
	update_miaoconfig();
    };
    tmp.placeholder = '添加好友网段(形如192.168.0.1/24), ' +
			'多个网段用回车分隔。';
    tmp.title = '这里填写的网段，在 miao 启动时会自动刷新一次。\n' +
		'点击“刷新好友”按钮会立即执行一次刷新。\n举例:\n' +
		'1. 某行填写“192.168.0.99”只刷新这一个 IP 地址。\n' +
		'2. 某行填写“192.168.0.99/24”会刷新 192.168.0.(0~255) 共' +
		' 256 个 IP 地址。\n理解其原理: 每个 IP 地址是 4 个字节, ' +
		'每个字节是 8 个位, 四个字节共有 32 位。\n在这 32 位中, ' +
		'左边 24 位 (即“192.168.0”的部分) 固定不变, \n剩下的 8 ' +
		'个位任意变化, 也就是说, 从 0 变到 255。\n' +
		'如果您熟悉二进制位的概念, 用上述第 2 种方式会很方便。在 ' +
		'x.x.x.x/y 的格式中,\n位数 y 可以是 8 ~ 32 之间的任意一个' +
		'数 (可以不是 8 的倍数, 比如可以是 23 或 25 等), 它越大,\n' +
		'就表示左边固定的部分越多, 因而剩余的可变部分就越少, 那么' +
		'要刷新的 IP 个数也就越少了。\n' +
		'程序禁止使用太小的 y 值, 以免刷新大量 IP, 造成网络瘫痪。\n' +
		'如果您只使用第 1 种方式, 那也是可行的, 但需要逐个列出您想' +
		'要刷新的全部 IP 地址, 可能会很长。\n两种方式可以混合使用。\n'
		+ '支持填写 IPv6 格式的单一地址，不带中括号，如：' +
		'240e:abcd::1234，但不支持附加“/子网掩码”后缀。\n' +
		'也支持填写域名，但域名必须能够解析为 IPv6 地址。';
    span_settings.appendChild (tmp);

    tmp = window.document.createElement ('input');
    tmp.type = 'button';
    tmp.value = '刷新好友';
    tmp.title = '在“更改设置”中可以设置要刷新的网段。\n' +
		'miao 启动时会自动刷新一次好友网段, \n' +
		'接下来再想刷新，必须手动点击此按钮。\n\n' +
		'若想在刷新前清空列表，请先关闭所有的聊天窗。\n' +
		'若想刷新时不清空列表，请保证至少存在一个聊天窗。';
    tmp.onclick = refresh;
    window.document.body.appendChild (tmp);

    window.document.body.appendChild (span_execwarn);

    // 好友列表
    const paltable = window.document.createElement ('table');
    paltable.id = 'paltable';
    //paltable.className = 'sortable';
    paltable.border = 1;
    paltable.style.borderStyle = 'none'; //也可 paltable.style.border = 'none';
    paltable.width = 2400;
    //paltable.tabIndex = 0; // 无需获取焦点，注释掉
    window.document.body.appendChild (paltable);
    window.document.body.onkeyup = function (e) {
	if (e.key == 'ArrowDown' || e.key == 'ArrowUp') {
	    if (! paltable_tbody)
		return;
	    if (paltable.childElementCount < 2 &&
		! paltable_tbody.childElementCount)
		return;
	    if (! paltable.contains(document.activeElement)) {
		if (paltable.childElementCount < 2) {
		    if (e.key == 'ArrowDown')
			paltable_tbody.firstElementChild.focus();
		    else
			paltable_tbody.lastElementChild.focus();
		    return;
		}
		if (e.key == 'ArrowDown') {
		    paltable.firstElementChild.firstElementChild.focus();
		    return;
		}
		if (paltable_tbody.lastElementChild)
		    paltable_tbody.lastElementChild.focus();
		else
		    if (paltable.firstElementChild.lastElementChild)
			paltable.firstElementChild.lastElementChild.focus();
		return;
	    }
	    if (paltable.childElementCount < 2) {
		if (e.key == 'ArrowDown') {
		    let next = document.activeElement.nextElementSibling;
		    if (! next)
			next = paltable_tbody.firstElementChild;
		    if (next)
			next.focus();
		    return;
		}
		let prev = document.activeElement.previousElementSibling;
		if (! prev)
		    prev = paltable_tbody.lastElementChild;
		if (prev)
		    prev.focus();
		return;
	    }
	    if (e.key == 'ArrowDown') {
		let next = document.activeElement.nextElementSibling;
		if (! next) {
		    if (paltable_tbody.contains(document.activeElement))
			next = paltable.firstElementChild.firstElementChild;
		    else
			next = paltable_tbody.firstElementChild;
		}
		if (next)
		    next.focus();
		return;
	    }
	    let prev = document.activeElement.previousElementSibling;
	    if (! prev) {
		if (paltable_tbody.contains(document.activeElement))
		    prev = paltable.firstElementChild.lastElementChild;
		else
		    prev = paltable_tbody.lastElementChild;
	    }
	    if (prev)
		prev.focus();
	    return;
	}
    };

    // 调试输出，解决使用 alert 时跳出对话框让程序中断运行的问题。

    //debug_info.id = 'debug_info';
    //debug_info.style.overflow = 'scroll';
    //debug_info.style.position = 'fixed';
    //debug_info.style.wordWrap = 'break-word';
    //debug_info.style.top = 0;
    //debug_info.style.left = 0;
    //debug_info.style.right = 0;
    //debug_info.style.bottom = 100;
    //debug_info.style.height = 'auto';
    //debug_info.style.maxHeight = 'auto';
    //debug_info.style.minHeight = 'auto';
    window.document.body.appendChild(debug_info);

    window.document.body.ondragover = function (e) {
	e.preventDefault();
    };
    window.document.body.ondrop = function (e) {
	e.preventDefault();

	if (! e.dataTransfer)
	    return;

	let files = e.dataTransfer.files;

	if (! files)
	    return;
	if (! files.length)
	    return;
	for (let i = 0; i < files.length ; i++) {
	    let f = files[i];
	    if (path.extname(f.path).toLowerCase() != '.eml')
		continue; // 文件扩展名不是 .eml, 忽略
	    if (! fs.existsSync(f.path))
		continue; // 文件不存在, 忽略
	    if (! fs.statSync(f.path).isFile())
		continue; // 不是普通文件, 忽略
	    // 解析 .eml 文件
	    recvMail_on_message ({
		event: 'parse',
		eml:	f.path,
	    });
	}

    };

    window.document.body.addEventListener('paste', (event) => {
	let data = (event.clipboardData || window.clipboardData);
	if (! data)
	    return;
	//let text = data.getData('text');
	//fail(data.items);
	//fail(data.files);
	//fail(event.dataTransfer);
	let files = data.files;

	if (! files)
	    return;
	if (! files.length)
	    return;
	for (let i = 0; i < files.length ; i++) {
	    let f = files[i];
	    if (path.extname(f.path).toLowerCase() != '.eml')
		continue; // 文件扩展名不是 .eml, 忽略
	    if (! fs.existsSync(f.path))
		continue; // 文件不存在, 忽略
	    if (! fs.statSync(f.path).isFile())
		continue; // 不是普通文件, 忽略
	    // 解析 .eml 文件
	    recvMail_on_message ({
		event: 'parse',
		eml:	f.path,
	    });
	}

	event.preventDefault();
    });

    var lastErrUDP = '';
    var lastErrTCP = '';
    udp_socket.on('error', (e) => {
	let err = e.toString();
	if (err != lastErrUDP) // 避免重复显示同一条错误信息
	    fail (err + ' 请关闭占用 UDP 2425 端口的程序。');
	lastErrUDP = err;

	// 在 socket 未打开的情况下执行 close 函数，错误隐蔽极难排查！参见: 
	// 《nodejs udp ERR_SOCKET_DGRAM_NOT_RUNNING 解决方法》
	// https://blog.csdn.net/u011249920/article/details/80182726
	// 必须在已经 open 的时候（即 bind 成功后）才能安全执行 close 操作。
	// 似乎 close 函数实用价值不高，大概只在程序退出时才会用到。
	if (udp_socket_opened) {
	    udp_socket.close();
	    udp_socket_opened = false;
	}
    });

    udp_socket.on('close', () => {
	// The 'close' event emits after a socket is closed with close().
	// Once triggered, no new 'message' events will emit on this socket.
	udp_socket_opened = false;
	// socket 关闭后，启动周期计时器尝试再次打开它
	// 每隔 10 秒执行一次 udp_socket_open 函数，尝试绑定 2425 端口。
	if (udp_socket_IntervalID == undefined)
	    udp_socket_IntervalID = setInterval (udp_socket_open, 10000);
    });

    udp_socket.on('listening', udp_socket_bind_listener);

    let test_C0_msg;
    const bin_header = '0:0:0:0:0:0\0\0\0\0\0';
    udp_socket.on('message', (msg, rinfo) => {
	// The 'message' event is emitted when a new datagram is available
	// on a socket. The event handler function is passed two arguments:
	// msg <Buffer> - The message.
	// rinfo <Object> - Remote address information:
	//	address <string> - The sender address.
	//	family <string> - The address family ('IPv4' or 'IPv6').
	//	port <number> - The sender port.
	//	size <number> - The message size.

	let ip = rinfo.address;
	let isIPv4 = false;
	let ip_changed = false;
	if (/:/.test(ip)) { // 处理 IPv6 格式的 IPv4 地址
	    if (/^::ffff:[a-fA-F0-9]{1,4}:[a-fA-F0-9]{1,4}$/i.test(ip)) {
		ip = ip.slice(7).split(':');
		ip[0] = parseInt(ip[0], 16);
		ip[1] = parseInt(ip[1], 16);
		ip = (ip[0] >>> 8) + '.' + (ip[0] & 255) + '.' +
		     (ip[1] >>> 8) + '.' + (ip[1] & 255);
		isIPv4 = true;
	    } else
	    if (/^::ffff:\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/i.test(ip)) {
		ip = ip.slice(7);
		isIPv4 = true;
	    } //else // 纯粹的 IPv6 地址，保持不变
		//ip = ip;
	}

	let isLocal = checkCIDR (ip);

	// 如下报头，表示二进制文件传输，跳过编码检查
	// 30 3A 30 3A 30 3A 30 3A 30 3A 30 00 00 00 00 00

	if (! Buffer.compare(msg.slice(0,16), Buffer.from(bin_header)))
	//if (msg.length > 15
	//	&& msg[ 0] == 0x30 && msg[ 1] == 0x3A
	//	&& msg[ 2] == 0x30 && msg[ 3] == 0x3A
	//	&& msg[ 4] == 0x30 && msg[ 5] == 0x3A
	//	&& msg[ 6] == 0x30 && msg[ 7] == 0x3A
	//	&& msg[ 8] == 0x30 && msg[ 9] == 0x3A
	//	&& msg[10] == 0x30 && msg[11] == 0x00
	//	&& msg[12] == 0x00 && msg[13] == 0x00
	//	&& msg[14] == 0x00 && msg[15] == 0x00)
	{
	    //fail ('收到二进制消息: msg.length = ' + msg.length);
	    if (msg.length < 128)
		return;
	    let id = msg.slice(16,32).toString();
	    if (isIPv4)
		id += '@' + ip + ':' + rinfo.port;
	    if (typeof (Pal[id]) !== 'object') {
		return;
	    }
	    if (! Pal[id].haskey)
		return;
	    Pal[id].offline = 0; // 设为在线
	    Pal[id].time = Date.now(); // 更新最近在线时间
	    updateTable(id);
	    if (msg.length == 128) // 对方收到文件片段后的应答
		return acknowledge (msg, id);
	    return recvBinaryData (msg, id);
	}

	const coding = getCodingStyle (msg);
	let msgEncode = coding.charset;
	let knownGB = coding.knownGB;
	let isMiao = coding.isMiao;
	if (! isMiao && GATEWAYs &&
		GATEWAYs.split(',').indexOf(ip) >= 0) {
	    return; // 忽略，不响应路由器内网的飞秋通过路由器周转的消息
	}
	if (msgEncode == 'unknown') {
	    if (coding.isHTTPU) {
		if (quit_ongoing > 0) {
		    quit_ongoing ++;
		} else if (quit_ongoing < 0) {
		    fail ('严重错误: quit_ongoing 为负数 ' + quit_ongoing);
		}
		// 异步执行 do_UPnP, 目的是立即用 return 返回, 让 quit_ongoing
		// 的改变能够尽快被别的（与 quit_app 有关的）异步代码捕捉到。
		setTimeout(do_UPnP, 0, msg, rinfo);
		return;
	    } else {
		fail ('收到未知软件从 ' + ip + ':' + rinfo.port +
		    ' 发来的 udp 消息: ' + JSON.stringify(msg.toString()));
		//return; // 忽略, 不再处理
		msgEncode = 'gb18030';
		knownGB = true;
		isMiao = false;
	    }
	}

	let MSG;
	let Msg;
	let ver_array;
	let logged;
	let isOuter;

	// 将 <Buffer> msg 转换为 <String> MSG
	if (msgEncode == 'utf8') {
	    MSG = msg.toString ('utf8');
	} else {
	    // 只转换开头两段
	    let i = msg.indexOf(0);
	    let j = -1;
	    if (i >= 0) {
		j = msg.indexOf(0, i+1);
	    }
	    if (j >= 0) {
		MSG = iconv.decode (msg.slice(0, j+1), msgEncode);
		MSG += msg.slice(j+1).toString('utf8');
	    } else {
		MSG = iconv.decode (msg, msgEncode);
	    }
	}

	// MSG 存放着好友发来的所有信息，包括 '\0' 字符
	const sections  = MSG.split ('\0');
	const header  = sections[0];
	const items = header.split (':', 5);

	//const Msg_bak = MSG;
	//if (items[4] & (IPMSG_CAPUTF8OPT | IPMSG_UTF8OPT))
	//	msgEncode = 'utf8';

	// IPMSG 的 login name 中使用了“< >”, 将其用于文件名时失败!
	Msg = {
	    version:	items[0],
	    SN:		items[1],
	    id:		//items[2].replace(/\\_[\'\"\s]/g,'-')
			//	.replace(/[<>]/g,'') + '@' +
			ip + ':' + rinfo.port,
	    host:	items[3],
	    cmd:	items[4],
	    mainbody:	header.replace (/([^:]*:){5}/, '')
	};
	ver_array = items[0].split('#');
	if (ver_array.length >= 8) {
	    if (ver_array[2].length >= 16) {
		// 检查是否十六进制数值
		// ....................
		Msg.id = ver_array[2];
		if (isIPv4)
		    Msg.id += '@' + ip + ':' + rinfo.port;
		// 本机多网卡的情况, 有些网卡收不到本机发送的消息,
		// 可以考虑在接收消息时不再区分是哪个网卡接收的。
		//Msg.id = ver_array[2];
	    } else if (ver_array[2].length >= 12) {
		// 检查是否十六进制数值
		// ....................
		Msg.id = ver_array[2] + '@' + ip + ':' + rinfo.port;
	    }
	}
	// IPMSG 的 login name 中使用了“< >”, 将其用于文件名时失败!
	if (/^.*<[0-9a-zA-Z]+>.*$/.test(items[2])) {
		Msg.id = items[2].replace(/^.*<(.*)>.*$/,'$1')
			+ '@' + ip + ':' + rinfo.port;
	}

	//const id = Msg.id;
	const cmd = Msg.cmd;

	let keyName = 'publicKey' + Msg.id.slice(0,16);
	let keyStr = miaoconfig[keyName];

	// 好友信息登记过了吗?
	logged = (Pal[Msg.id] ? true : false);

	if (! logged)
	    Pal[Msg.id] = {};

	const pal = Pal[Msg.id];

	if (! logged) // 好友是首次被添加
	{
	    // 昵称和工作组是后来扩展的字段, 不总是有, 先设为默认的初始值
	    pal.nick = 'noName';
	    pal.wgroup = '';
	}

	function update_friends () {
	    if (! isMiao)
		return;
	    if (! /:/.test(pal.IP))
		return;
	    if (! /:/.test(pal.myIPs))
		return;
	    let friends = stored_friends.split(/[\r\n]+/);
	    for (let i in friends) {
		if (friends[i].replace(/[\s]+/g, '') == '')
		    continue; // 跳过空白行
		let v = friends[i].split(/\s+/);
		if (pal.version.split('#')[8] ==
			v[0].replace(/@ethereal\.email$/, '') &&
			pal.version.split('#')[2] == v[1]) {
		    friends[i] = v[0] + ' ' + v[1] + ' ' +
				 (pal.nick || v[2] || 'none') + ' ' +
				 (pal.wgroup || v[3] || 'none') + ' ' +
				 pal.IP + ' ' + pal.myIPs;
		    // GUAs 历史数据结构: IP地址, 掩码位数, 使用频度, 使用日期
		    if (! miaoconfig[Msg.id].GUAsCache)
			miaoconfig[Msg.id].GUAsCache = {};
		    let myIPs = pal.myIPs.split(',');
		    for (let j = 0; j < myIPs.length; j++) {
			let addr = myIPs[j].replace(/\/\d+$/, '');
			let mask = Number(myIPs[j]
			       .replace(/^[a-fA-F0-9:]+\/+/, ''));
			let info = miaoconfig[Msg.id].GUAsCache[addr];
			if (! info ||
			    ! Array.isArray(info) ||
			    info.length != 3 ||
			    typeof info[0] !== 'number' ||
			    typeof info[1] !== 'number' ||
			    typeof info[2] !== 'number') {
			    if (info)
				delete miaoconfig[Msg.id].GUAsCache[addr];
			    miaoconfig[Msg.id].GUAsCache[addr] = [];
			    miaoconfig[Msg.id].GUAsCache[addr].push(mask);
			    miaoconfig[Msg.id].GUAsCache[addr].push(1);
			    miaoconfig[Msg.id].GUAsCache[addr]
						.push(Date.now());
			} else {
			    miaoconfig[Msg.id].GUAsCache[addr].push(mask);
			    miaoconfig[Msg.id].GUAsCache[addr].push(info[1]+1);
			    miaoconfig[Msg.id].GUAsCache[addr]
						.push(Date.now());
			}
		    }
		    // 只保留 60 个元素
		    while (Object.keys(miaoconfig[Msg.id].GUAsCache)
					.length > 60) {
			// 丢弃一项
			let discard;
			for (let addr in miaoconfig[Msg.id].GUAsCache) {
			    if (! discard) {
				discard = addr;
				continue;
			    }
			    let info1 = miaoconfig[Msg.id].GUAsCache[addr];
			    let info2 = miaoconfig[Msg.id].GUAsCache[discard];
			    if (info1[1] > info2[1])
				continue;
			    if (info1[1] < info2[1]) { // 使用频度更少
				discard = addr;
				continue;
			    }
			    if (info1[0] < info2[0])
				continue;
			    if (info1[0] > info2[0]) { // 掩码位数更大
				discard = addr;
				continue;
			    }
			    if (info1[2] > info2[2])
				continue;
			    if (info1[2] < info2[2]) { // 登记日期更老
				discard = addr;
				continue;
			    }
			}
			delete miaoconfig[Msg.id].GUAsCache[discard];
		    }
		}
	    }
	    // 更新好友设置
	    let tmp = window.document.getElementById('friends');
	    tmp.value = friends.join('\n');
	    tmp.onchange();
	} // 局部函数 update_friends () 结束

	// IP 会在 myIPs 的范围内不断变化，注释掉
	//if (! isIPv4 && pal.IP != ip)
	//    ip_changed = true;
	pal.IP = ip;
	pal.port = rinfo.port;

	if (isMiao && isIPv4 && isLocal && ADDRs.split(',').indexOf(ip) < 0
		&& ver_array[2] == stored_randomID) {
		if (GATEWAYs) {
		    if (GATEWAYs.split(',').indexOf(ip) < 0)
			GATEWAYs += (',' + ip);
		} else {
			GATEWAYs = ip;
		}
	}

	if (! logged) {
	    pal.login = items[2];
	    pal.host = Msg.host;
	} else {
	    if (items[2] != 'Doe')
		pal.login = items[2];
	    if (Msg.host != 'noHost')
		pal.host = Msg.host;
	}

	pal.charset = msgEncode; // 字符集是随着客户端软件的变化而变的
	pal.version = items[0]; // 版本串也是随客户端不同而变化的
	if (! pal.chatwin)
	    pal.chatwin = 0;

	if (! pal.myIPs)
	    pal.myIPs = '';
	if (! pal.yourIP)
	    pal.yourIP = '';
	if (! pal.yourPort)
	    pal.yourPort = '';
	if (! pal.services)
	    pal.services = '';

	pal.time = Date.now();
	if (! pal.mediacy)
	    pal.mediacy = '';
	pal.haskey = (keyStr ? 1 : (isMiao ? -1 : 0));
	pal.offline = 0; // 先设为在线吧，既然已经发来消息了
	if (pal.recv == undefined)
	    pal.recv = 0;
	if (pal.send == undefined)
	    pal.send = 0;

	// 针对某台测试机的调试, 会输出全部消息
	// 在主窗口左上角的探测ip框里填写ip地址, 就会输出这个ip发来的全部消息
	// 如果同时清空探测端口框里的 2425, 则表示会显示十六进制格式以便查看
	let detip = window.document.getElementById('detectip').value;
	let detport = window.document.getElementById('detectport').value;

	if (ip == detip) {
	    fail ('【 ' +
		('00000000' + Number(cmd).toString(16)).slice(-8) + ' 】' +
		JSON.stringify(MSG).replace(/</g, '&lt;').replace(/>/g, '&gt;')
		+ ((detport != '') ? '' : ('<br/>' + msg.toString('hex'))));
	}
	// 若发送者 ip 不属于本地局域网，而且发送者看到的你的端口号不是 2425,
	// 或者发送者看到的你的 ip 不是局域网 ip, 就说明发送者是来自外网
	isOuter = (! isLocal && (pal.yourPort != 2425 ||
		ADDRs.split(',').indexOf(pal.yourIP) == -1));

	// 扩展功能。低字节为 0 时，其它飞鸽类软件会当作 no operation 忽略掉。
	// 为尽量避免与其它飞鸽软件发生冲突, miao 用低字节 0 来作为功能扩展。

	if (cmd == (IPMSG_BR_ENTRY << 8)) { // 低字节为 0, 是扩展功能
	    if (! isMiao)
	        return;
	    if (! isIPv4)
	        return;
	    // 收到转播的上线消息
	    updateTable(Msg.id); // 需要更新转发者信息
	    let fw_addr = sections[1];	// 原始发送者 ip
	    if (! fw_addr)
		return;
	    if (/:/.test(fw_addr)) // 忽略 IPv6 地址的转发
		return;
	    let fw_port = sections[2];	// 原始发送者 port
	    msgEncode = sections[3];	// 原始发送者的字符集编码
	    let Item = sections[4].split(':',5);

	    // 更改 Msg
	    Msg.version = Item[0];
	    Msg.SN = Item[1];
	    Msg.id = Item[2].replace (/\\_[\'\"\s]/g,'-')
			+ '@' + fw_addr + ':' + fw_port;
	    Msg.host = Item[3];
	    Msg.cmd = Item[4];
	    Msg.mainbody = sections[4].replace (/([^:]*:){5}/, '');

	    ver_array = Item[0].split('#');
	    if (ver_array.length >= 8) {
		if (ver_array[2].length >= 16) {
			// miao 使用 16 字节表示随机生成的机器 ID
			// 检查是否十六进制数值
			// ....................
			Msg.id = ver_array[2]+'@'+fw_addr+':'+fw_port;
		} else if (ver_array[2].length >= 12) {
			// 飞秋使用 12 字节表示网卡物理地址
			// 检查是否十六进制数值
			// ....................
			Msg.id = ver_array[2]+'@'+fw_addr+':'+fw_port;
		} else // 转发数据异常，退出
		    return;
	    } else // 转发数据异常，退出
		return;

	    logged = (Pal[Msg.id] ? true : false);

	    if (! logged) Pal[Msg.id] = {};

	    if (! Pal[Msg.id].nick || Pal[Msg.id].nick == 'noName') {
	        Pal[Msg.id].nick = Msg.mainbody
			.replace(/[\u0000-,/:-@[-`{|}\u007f]/g, '_');
	        Pal[Msg.id].wgroup = sections[5]
			.replace(/[\u0000-,/:-@[-`{|}\u007f]/g, '_');
	    }
	    Pal[Msg.id].IP = fw_addr;
	    Pal[Msg.id].port = fw_port;
	    if (! logged) {
		Pal[Msg.id].login = items[2];
		Pal[Msg.id].host = Msg.host;
	    } else {
		if (items[2] != 'Doe')
		    Pal[Msg.id].login = items[2];
		if (Msg.host != 'noHost')
		    Pal[Msg.id].host = Msg.host;
	    }

	    Pal[Msg.id].charset = msgEncode;
	    Pal[Msg.id].version = Item[0];
	    if (! Pal[Msg.id].chatwin)
		Pal[Msg.id].chatwin = 0;
	    if (! Pal[Msg.id].myIPs)
		Pal[Msg.id].myIPs = sections[8];
	    if (Pal[Msg.id].yourIP == undefined)
		Pal[Msg.id].yourIP = '';
	    if (Pal[Msg.id].yourPort == undefined)
		Pal[Msg.id].yourPort = '';
	    if (! Pal[Msg.id].services)
		Pal[Msg.id].services = '';
	    Pal[Msg.id].time = Date.now();
	    if (Pal[Msg.id].yourIP != ip &&
	        Pal[Msg.id].yourPort != rinfo.port)
		Pal[Msg.id].mediacy = ip + ':' + rinfo.port;
	    else if (Pal[Msg.id].mediacy == undefined)
		Pal[Msg.id].mediacy = '';
	    if (Pal[Msg.id].haskey == undefined)
		Pal[Msg.id].haskey = (isMiao ? -1 : 0);
	    Pal[Msg.id].offline = 0;
	    if (Pal[Msg.id].recv == undefined)
		Pal[Msg.id].recv = 0;
	    if (Pal[Msg.id].send == undefined)
		Pal[Msg.id].send = 0;

	    // 回应对方, 建立通讯链路。若我是罩在路由器下, 这点尤其重要。
	    sendMsg(me, Pal[Msg.id], IPMSG_ANSENTRY, me.nick);

	    updateTable(Msg.id); // 这是更新原始发送者的信息

	    // 若转发者在外网，继续在本网转发上线消息
	    // 这种情况发生在外网也是罩在路由器下的情况。我处于内层路由器
	    // 之下, 外层可能有多重路由器。此时, 我可能是 DMZ 主机, 那么,
	    // 我需要继续转发上级路由器转发过来的某人上线的消息。

	    if (stored_outer_LAN_IP &&
		stored_outer_LAN_IP.replace (/\/.*$/, '') != ip)
	    if (isOuter)
	    {
		//fail('转发者在外网，继续在本网转发上线消息');
		refresh(false, MSG.replace(/^[^\0]*\0/,'\0'));
	    }
	    return;
	}

	if (cmd == (IPMSG_BR_EXIT << 8)) { // 低字节为 0, 是扩展功能
	    if (! isMiao)
	        return;
	    if (! isIPv4)
	        return;
	    // 收到转播的离线消息
	    updateTable(Msg.id); // 需要更新转发者信息
	    let fw_addr = sections[1];	// 原始发送者 ip
	    if (! fw_addr)
		return;
	    if (/:/.test(fw_addr)) // 忽略 IPv6 地址的转发
		return;
	    let fw_port = sections[2];	// 原始发送者 port
	    msgEncode = sections[3];	// 原始发送者的字符集编码
	    let Item = sections[4].split(':',5);

	    // 更改 Msg
	    Msg.version = Item[0];
	    Msg.SN = Item[1];
	    Msg.id = Item[2].replace (/\\_[\'\"\s]/g,'-')
			+ '@' + fw_addr + ':' + fw_port;
	    Msg.host = Item[3];
	    Msg.cmd = Item[4];
	    Msg.mainbody = sections[4].replace (/([^:]*:){5}/, '');

	    ver_array = Item[0].split('#');
	    if (ver_array.length >= 8) {
		if (ver_array[2].length >= 16) {
			// miao 使用 16 字节表示随机生成的机器 ID
			// 检查是否十六进制数值
			// ....................
			Msg.id = ver_array[2]+'@'+fw_addr+':'+fw_port;
		} else if (ver_array[2].length >= 12) {
			// 飞秋使用 12 字节表示网卡物理地址
			// 检查是否十六进制数值
			// ....................
			Msg.id = ver_array[2]+'@'+fw_addr+':'+fw_port;
		} else // 转发数据异常，退出
		    return;
	    } else // 转发数据异常，退出
		return;

	    if (Pal[Msg.id]) {
		Pal[Msg.id].offline = 1; // 标记原始发送者离线
		Pal[Msg.id].time = Date.now();
		updateTable(Msg.id); // 这是更新原始发送者的信息
	    }

	    // 若转发者在外网，继续在本网转发离线消息
	    // 这种情况发生在外网也是罩在路由器下的情况。我处于内层路由器
	    // 之下, 外层可能有多重路由器。此时, 我可能是 DMZ 主机, 那么,
	    // 我需要继续转发上级路由器转发过来的某人离线的消息。

	    if (stored_outer_LAN_IP &&
		stored_outer_LAN_IP.replace (/\/.*$/, '') != ip)
	    if (isOuter)
	    {
		//fail('转发者在外网，继续在本网转发离线消息');
		refresh(true, MSG.replace(/^[^\0]*\0/,'\0'));
	    }
	    return;
	}

	if (cmd == (IPMSG_ANSPUBKEY << 8)) { // 低字节为 0, 是扩展功能
	    if (! isMiao)
		return;
	    // 收到对方发来的 key
	    if (! effective_mailuser || ! effective_mailpass)
		return; // 自己无邮箱，忽略发来的 key
	    let account = pal.version.split('#')[8];
	    if (! account)
		return; // 对方无邮箱，忽略发来的 key
	    if (! /@/.test(account))
		account += '@ethereal.email';
	    // 查询是否好友
	    let tmp = stored_friends.split(/\s[^\r\n]*[\r\n]{0,}/);
	    if (tmp.indexOf(account) == -1)
		return; // 对方不是好友，忽略发来的 key
	    // 是好友，接受发来的 key
	    if (! keyStr) {
		miaoconfig[keyName] = sections[1];
		miaoconfig_needupdate = true;
		update_miaoconfig();
	    }
	    pal.haskey = 1;
	    updateTable(Msg.id);
	    return;
	}

	// IPMSG_CAPFILEENC_OBSLT= 0x00001000; // 有文件加密的能力(废弃)
	if (cmd == IPMSG_CAPFILEENC_OBSLT) { // 低字节为 0, 是扩展功能
	    // 收到请求文件夹同步的消息
	    if (! isMiao)
		return;
	    dir_sync_start();
	    return;
	}

	switch (cmd & 255) {
	// case 0:
	//	// 这是 no operation, 其它飞鸽类软件会忽略该操作。
	//	// 最低字节为 0 时, miao 把其余 3 字节整数用于扩展功能。
	//	// 具体解析的代码紧接着现在的 switch 代码块之前。
	//    break;
	case IPMSG_GETFILEDATA: // 对方 miao 用户请求用 udp 传输文件
	    updateTable(Msg.id); // 表格要更新
	    if (! isMiao) {
		fail ('未知软件请求 udp 传输文件, 来自 ' + pal.nick
			+ '@' + ip + ':' + rinfo.port +
			', 原始消息:<br/>' + JSON.stringify(MSG));
		break; // 忽略其它软件发来的异常请求
	    }
	    const v = Msg.mainbody.split(':');
	    let packetID = parseInt(v[0], 16);
	    if (isNaN(packetID)) {
		break; // 忽略错误的请求
	    }
	    // 获取当初我发送文件时的“选择文件”按钮对象
	    let input = SendFile[packetID];
	    if (! input // || ! input.value || input.value == ''
			|| ! input.files || ! input.files.length) {
		break; // 忽略错误的请求
	    }

	    let count = input.files.length; // 当初我发送文件的总数
	    let fileNo = parseInt(v[1], 16) - 1; // 对方请求发送的文件序号
	    if (isNaN(fileNo) || fileNo < 0 || fileNo >= count) {
		break; // 忽略错误的请求
	    }
	    let offset = parseInt(v[2], 16); // 对方请求发送的偏移位置
	    if (isNaN(offset) || offset < 0 ||
				 offset > Number.MAX_SAFE_INTEGER
		|| (offset % PLEN)) {
		break; // 忽略错误的请求
	    }

	    let chatwin = input.ownerDocument.defaultView;
	    const f = input.files[fileNo];	// 要发送的文件
	    const f_size = f.size;		// 文件长度
	    const f_time = f.lastModified;	// 文件修改时间

	    let endpos; // 结束偏移量(字节数; 注意, 是不含)
	    if (v[3] == '')
		endpos = f_size;
	    else {
		endpos = parseInt(v[3], 16);
		if (isNaN(endpos) || endpos < 0 ||
			endpos > Number.MAX_SAFE_INTEGER)
		    break; // 忽略错误的请求
	    }

	    // 告诉对方我已收到消息, 正在发送数据
	    // to do........

	    if (! pal.send)
		pal.send = {};

	    if (! pal.send[fileNo])
		pal.send[fileNo] = {};

	    const sendfile = pal.send[fileNo];

	    if (typeof sendfile !== 'object' || ! sendfile) {
		alert ('严重错误: sendfile 未定义, 或其类型不是 object:\n' +
			'sendfile = ' + JSON.stringify(sendfile));
		break; // 异常退出
	    }

	    // 记录对方发来的消息序号, 同一个序号的消息可以发送多次
	    // 已经收到的消息序号, 不再重复处理

	    if (sendfile.done_SN && sendfile.done_SN[Msg.SN])
		break; // 已处理过, 退出

	    if (typeof sendfile.monitor !== 'undefined') {
		delete sendfile.monitor;
	    }
	    if (typeof sendfile.pieces !== 'undefined') {
		delete sendfile.pieces;
	    }

	    // 清空其它属性，只保留 done_SN
	    //delete sendfile.monitor;
	    //delete sendfile.pieces;

	    if (! sendfile.done_SN)
		sendfile.done_SN = {};

	    sendfile.done_SN[Msg.SN] = true;

	    sendfile.monitor = [];

	    // 从文件的偏移 offset 处开始发送, 到偏移 endpos (不含)为止

	    let div_out = input.ownerDocument.getElementById('outputbox');

	    let p = input.ownerDocument.createElement('p');
	    p.innerHTML = '<span style="color:green">' + (new Date())
		.toLocaleString(date_locale, date_options) + ' 向 ' +
		pal.nick + '(' + Msg.id +
		') 发送文件 ' + f.path + ', 长度 ' + f_size + ' 字节; ' +
		(offset ? ('对方请求断点续传, 跳过头部 ' + offset +
		' 个字节; ') : '') +
		'已发送 </span><span style="color:blue">0%</span>';
	    //p.style.color = 'blue';
	    p.style.margin = 0;
	    //p.style.padding = 0;
	    div_out.appendChild(p);
	    p.scrollIntoView(false);
	    save_history(p.innerHTML, Msg.id);

	    let fd;
	    let intervalID;
	    let sending_end = function () {
		if (! intervalID) // 避免多次重入
		    return; 
		// 停止进度监视
		clearInterval(intervalID);
		intervalID = null;

		if (chatwin.sending > 0)
		    chatwin.sending--;

		// 设定失败标志, 通知所有的监控器不必再发送任何片段了
		error_read = true;

		// 关闭已经打开的文件描述符
		try {
		    if (fd != undefined)
			fs.closeSync(fd);
		} catch (e) {
		    fail ('关闭文件失败: ' + e);
		}
		fd = undefined;
		sending_end = null; // 再次保证此函数只执行一次
	    }

	    let notify_failure = function (i, p) {
		let Buf = Buffer.alloc(160); // 长度须为 160 才表示失败
		Buf.write(bin_header + stored_randomID, 0); // 32 字节
		Buf.writeDoubleLE(packetID, 32);// 消息序号, 8 字节
		Buf.writeUInt16LE(fileNo, 40);	// 文件序号, 2 字节
		Buf.writeDoubleLE(f_size, 48);	// 文件长度, 8 字节
		Buf.writeDoubleLE(f_time, 56);	// 修改时间, 8 字节 
		Buf.writeUInt32LE(i, 64);	// 监控器号, 4 字节
		Buf.writeUInt32LE(p, 72);	// 片段序号, 4 字节
		Buf.writeUInt16LE(0, 80);	// 片段长度, 2 字节
		//udp_socket.send (Buf, pal.port, pal.IP, (e)=>
		// rinfo.address 就是原始的 IPv6 格式的地址
		udp_socket.send (Buf, pal.port, rinfo.address, (e)=>{
		    if (e)
			fail ('向对方发送失败通告也失败了! ' + e);
		});
		if (typeof sending_end === 'function')
			sending_end();
	    }

	    // nw 和 electron 都支持 f.path 获取真实路径
	    try {
		fd = fs.openSync(f.path, 'r'); // 打开文件，准备读取数据
	    } catch (e) {
		fail ('打开文件 ' + f.path + ' 失败, 无法发送。' + e);
		notify_failure(0, 0);
		break; // 退出
	    }

	    // 为减轻网络负担, 设置 N = 1
	    let N = 1;	// 有 N 个监控器异步发送文件片段
			// 如需按顺序发送片段，必须设置 N = 1
	    const start_p = offset / PLEN;		// 起始片段号
	    const end_p = Math.ceil(endpos / PLEN);	// 终止片段号(不含)
							// 故，它也是片段总数
	    if (N > end_p)	// 监控器个数超过片段总数
	        N = end_p;	// 向下调整监控器个数
	    let next_p = start_p + N;
	    sendfile.pieces = start_p;
	    let error_read = false; // 出现错误时, 所有的监控器都将停止工作

	    // 定义局部函数, 用监控器 i 来发送片段 p
	    function send_piece (i, p) {
	        const p_bytes = p * PLEN;
		//if (p * PLEN >= endpos)
		//    return;
		if (error_read) { // 任一片段失败, 所有的监控器都退出
		    return;
		}
		let len = (endpos - p_bytes);
		if (len > PLEN)
		    len = PLEN;
		let buf = Buffer.alloc(128 + len);

		// 读片段 p 到 buf + 128
		fs.read(fd, buf, 128, len, p_bytes, (err, len1, buf1) => {
		    if (err) {
			fail ('读片段 ' + p + ' 失败: ' + err);
			error_read = err; // 此后所有的片段都不再发送了
			try {
			    if (fd != undefined)
				fs.closeSync(fd);
			} catch (e) {
			    fail ('关闭文件失败: ' + e);
			}
			fd = undefined;
			//向接收者发送失败消息包, 片段长度为 0 表示失败
			notify_failure(i, p);
			return;
		    }

		    const hash = crypto.createHash('sha256');
		    hash.update(buf.slice(128));
		    let digest = hash.digest();
		    buf.write(bin_header + stored_randomID, 0);	// 32 字节
		    buf.writeDoubleLE(packetID, 32);	// 消息序号, 8 字节
		    buf.writeUInt16LE(fileNo, 40);	// 文件序号, 2 字节
		    buf.writeDoubleLE(f_size, 48);	// 文件长度, 8 字节
		    buf.writeDoubleLE(f_time, 56);	// 修改时间, 8 字节 
		    buf.writeUInt32LE(i, 64);		// 监控器号, 4 字节
		    buf.writeUInt32LE(p, 72);		// 片段序号, 4 字节
		    buf.writeUInt16LE(len, 80);		// 片段长度, 2 字节
		    buf.fill(digest, 96, 128);		// 片段摘要, 32 字节
		    let repeat_count = 8; // 尝试发送同一片段的次数

		    // 内嵌函数, 方便失败时用 setTimeout 来重试同一个发送步骤
		    function send () {
			if (error_read)	{ //任一片段出错, 所有的片段都停止发送
			    return;
			}

			if (sendfile.monitor[i].p != null &&
			    sendfile.monitor[i].p >= p) {
			    // 对方已成功接收片段, 咱停止发送
			    return;
			}
			repeat_count--;
			if (! repeat_count) {
			    // 因网络等故障发送失败, 停止发送, 避免无限循环
			    error_read = true; // 通知所有的监控器不再发送了
			    notify_failure(i, p);
			    return;
			}
			//udp_socket.send(buf,pal.port, pal.IP,
			// rinfo.address 就是原始的 IPv6 格式的地址
			udp_socket.send (buf, pal.port, rinfo.address,
			    function (e) {
				if (e) {
				    fail ('发送片段 ' + p + ' 失败: ' + e);
				}
			    });

			// 检查对方是否成功接收片段, 必要时重发
			setTimeout(send, (8 - repeat_count) * 1000);
			//setTimeout( () => {
			//    if (sendfile.monitor[i].p == null ||
			//	sendfile.monitor[i].p < p) {
			//	//fail ('重发片段 ' + p);
			//	send(); // 对方未收到, 重新发送此片段
			//    } else {
			//	//fail ('对方成功接收片段 ' + p);
			//    }
			//}, (30 - repeat_count) * 100);
		    } // send () 内嵌函数结束

		    sendfile.monitor[i].cb = function () {
			if (sendfile.monitor[i].p < 0 || this.p < 0)
			{
			    // 接收方无法接收
			    error_read = true; // 通知所有的监控器不再发送了
			    // 不执行 notify_failure(), 只执行 sending_end();
			    if (typeof sending_end === 'function')
				sending_end();
			    return;
			}
			// 对方接收成功, 发送下一个片段
			//fail ('对方收到片段 ' + p + ', 发送片段 ' + next_p);
			sendfile.pieces++;
			if (next_p < end_p) {
			    send_piece(i, next_p++);
			} else {
			    // 本监控器闲置，活动的监控器总数减少一个
			    N--; // 活动的监控器总数用来计算 Timeout 超时间隔
			    loops = 0; // N 变了, setInterval 的 loops 要清零
			}
		    };
		    send(); // 发送片段, 自动跟踪失败并由 setTimeout 来重试
		}); // fs.read() 读片段 p 结束
	    } // send_piece(i, p) 局部函数定义结束

	    // 每隔半秒更新一次进度百分比。
	    // 监视发送是否已完成或失败, 此时关闭已经打开了的文件
	    let loops = 0;
	    let old_pieces = -1;
	    intervalID = setInterval (function () {
		loops++;
		p.firstChild.nextElementSibling.innerHTML =
		    (parseInt(sendfile.pieces / end_p * 100000) / 1000) + '%';
		if (sendfile.pieces >= end_p) { // 成功, 更新已接收人数
		    Pal[chatwin.pal_id].send.input.files[fileNo].recv ++;
		    input.ownerDocument.getElementById('filesToSend')
			.rows[fileNo].cells[0]
			.innerHTML -= -1; // 用加法时会当成字符串合并
		} else if (loops % (4 * N + 120))
		    return;

		// 发送已完成; 或者若长时间没能成功发送一个片段, 则判定为失败
		// 这里不严格区分成功和失败; 两者都是执行结束发送的操作
		if (sendfile.pieces >= end_p || old_pieces >= sendfile.pieces)
		    if (typeof sending_end === 'function')
			sending_end();

		old_pieces = sendfile.pieces;
	    }, 500);

	    // 初始时，启动 N 个监控器
	    for (let i = 0; i < N; i++) {
	        sendfile.monitor.push({});
		send_piece(i, start_p + i);
	    }
	    // 发送已经开始, 设置 chatwin.sending
	    if (chatwin.sending < 0)
		chatwin.sending = 0;
	    chatwin.sending++;
	    break;
	case IPMSG_ANSENTRY: // 我刚才上线打招呼, 对方回应了
	    if (Msg.mainbody != 'noName') {
		pal.nick = Msg.mainbody
			.replace(/[\u0000-,/:-@[-`{|}\u007f]/g, '_');
		pal.wgroup = sections[1]
			.replace(/[\u0000-,/:-@[-`{|}\u007f]/g, '_');
	    }
	    if (! logged) { // 好友首次被添加, 向其通告 Doe 离线, me 在线
		sendMsg(Doe, pal, IPMSG_BR_EXIT, Doe.nick);
		sendMsg(me, pal, IPMSG_BR_ENTRY, me.nick);
	    }
	    if (! isMiao) {
		updateTable(Msg.id);
		break;
	    }
	    if (sections[4]) {
		if (! isIPv4 && pal.myIPs != sections[4])
		    ip_changed = true;
		pal.myIPs = sections[4];
	    }
	    if (sections[5] && sections[5] != '255.255.255.255')
		pal.yourIP = sections[5];
	    if (sections[6] && sections[6] != '')
		pal.yourPort = sections[6];
	    if (sections[8]) {
		pal.services = sections[8];
	    }

	    updateTable(Msg.id);

	    //dir_sync_start(); // miao 用户应答了, 检查同步

	    if (sections[7] == 'askKey') { // 对方请求发送key
		if (! effective_mailuser || ! effective_mailpass)
		    break; // 自己无邮箱，拒绝发送
		let account = pal.version.split('#')[8];
		let tmp;
		if (! account)
		    break; // 对方无邮箱，拒绝发送
		if (! /@/.test(account))
		    account += '@ethereal.email';
		// 查询是否已经为好友
		tmp = stored_friends.split(/\s[^\r\n]*[\r\n]{0,}/);
		if (tmp.indexOf(account) == -1)
		    break; // 对方不是好友，拒绝发送
		// 是好友，执行发送
		sendMsg(me, pal, (IPMSG_ANSPUBKEY << 8),
			me.nick + '\0' + miaoconfig.publicKey);
	    }
	    break;
	case IPMSG_BR_ENTRY: // 有人上线, 要求我必须给出回应
	    if (Msg.mainbody != 'noName') {
		pal.nick = Msg.mainbody
			.replace(/[\u0000-,/:-@[-`{|}\u007f]/g, '_');
		if (sections[1])
		    pal.wgroup = sections[1]
			.replace(/[\u0000-,/:-@[-`{|}\u007f]/g, '_');
	    }
	    if (sections[2]) {
		let v = sections[2].split(/[\r\n]+/);
		for (let i = 0; i < v.length; i++) {
		    let tmp = v[i].slice(3)
				.replace(/[\u0000-,/:-@[-`{|}\u007f]/g, '_');
		    if (v[i].slice(0,3) == 'UN:') {
			if (tmp != 'Doe')
			    pal.login = tmp;
		    } else if (v[i].slice(0,3) == 'HN:') {
			if (tmp != 'noHost')
			    pal.host = tmp;
		    } else if (v[i].slice(0,3) == 'NN:')
			pal.nick = tmp;
		    else if (v[i].slice(0,3) == 'GN:')
			pal.wgroup = tmp;
		}
	    }
	    sendMsg(me, pal, IPMSG_ANSENTRY, me.nick);

	    if (! logged) { // 好友首次被添加, 向其通告 Doe 离线, me 在线
		sendMsg(Doe, pal, IPMSG_BR_EXIT, Doe.nick);
		sendMsg(me, pal, IPMSG_BR_ENTRY, me.nick);
	    }
	    if (! isMiao) {
		updateTable(Msg.id);
		break;
	    }
	    if (sections[4]) {
		if (! isIPv4 && pal.myIPs != sections[4])
		    ip_changed = true;
		pal.myIPs = sections[4];
	    }
	    if (sections[5] && sections[5] != '255.255.255.255')
		pal.yourIP = sections[5];
	    if (sections[6] && sections[6] != '')
		pal.yourPort = sections[6];
	    if (sections[8]) {
		pal.services = sections[8];
	    }

	    updateTable(Msg.id);

	    //dir_sync_start(); // miao 用户上线了, 检查同步

	    if (! isIPv4) // 对方为 IPv6，不为其转播上线消息
		break;
	    isOuter = (! isLocal && (pal.yourPort != 2425 ||
		ADDRs.split(',').indexOf(pal.yourIP) == -1));
	    // 若对方在外网，替对方在本网转播一次上线消息

	    // 若对方 ip 不属于本地局域网，而且
	    // 对方看到的你的端口号不是 2425，
	    // 或者对方看到的你的 ip 不是局域网 ip
	    // 就说明对方是在外网
	    if (stored_outer_LAN_IP &&
		stored_outer_LAN_IP.replace (/\/.*$/, '') != ip)
	    if (isOuter) {
		refresh(false, '\0' +
			ip + '\0' +
			rinfo.port + '\0' +
			pal.charset + '\0' +
			MSG);
	    }

//	    if ((cmd >>> 8) == IPMSG_BR_ENTRY ) {
//		// 中介收到必须转发的上线消息，逐个转发给所有在线客户
//		for (let p in Pal) {
//		    // 先向你报告对方上线，以便你回应对方，打开通路
//		    sendMsg(me, pal, (IPMSG_BR_ENTRY << 8),
//			me.nick + '\0' +
//			Pal[p].IP + '\0' +
//			Pal[p].port + '\0' +
//			Pal[p].charset + '\0' +
//			Pal[p].version + ':0:' + Pal[p].id + ':' +
//			Pal[p].host + ':' +
//			IPMSG_BR_ENTRY + ':' +
//			Pal[p].nick + '\0' +
//			Pal[p].wgroup + '\0\0utf-8\0' +
//			Pal[p].myIPs + '\0' +
//			ip + '\0' +
//			rinfo.port + '\0');
//
//		    // 然后向对方报告你上线，对方回应你时，连接已经建立。
//		    sendMsg(me, Pal[p], (IPMSG_BR_ENTRY << 8),
//			me.nick + '\0' +
//			ip + '\0' +
//			rinfo.port + '\0' +
//			pal.charset + '\0' +
//			MSG);
//		}
//	    }

	    break;
	case IPMSG_SENDMSG: // 有人发来消息
	    // 如果是飞秋发送的图片, 告诉对方, 图片无法接收。
	    if (/\/~#>[0-9a-fA-F]{8}<B~/.test(Msg.mainbody)) {
	        let reply = Msg.mainbody;

		Msg.mainbody = Msg.mainbody
				.replace(/\/~#>([0-9a-fA-F]{8})<B~/,
					'【图片$1】');
		setTimeout (sendMsg, 50, me, Msg.id, IPMSG_SENDMSG,
			'我在用 miao, 无法接收你刚才从飞秋的打字框里发送' +
			'的加密图片。请设法把图片保存为硬盘上的图片文件, ' +
			'然后把它拖到打字框右侧的“发送文件”框里再发给我。');

//		// 以下的测试表明, 飞秋发过来的加密图片，是可以原封不动发回去的
//		let s = '1_lbt6_0#131200#' + stored_randomID +
//			'#0#0#0#4001#9#:' + send_SN + ':' + login_name + ':' +
//			me.host + ':2097344:'; // 0x2000c0
//		test_C0_msg = undefined;
//		let try_send = function () {
//			if (typeof test_C0_msg === 'undefined') {
//				setTimeout (try_send, 0);
//				return;
//			}
//			let count = 0;
//			let i;
//			for (i = 0; i < test_C0_msg.length; i++) {
//				if (test_C0_msg[i] == 0x3A)
//					count++;
//				if (count >= 5)
//					break;
//			}
//			if (count < 5)
//				fail ('飞秋 C0 消息缺少冒号!');
//			else {
//				// 发送一条图片消息给飞秋
//				sendMsg(me, Msg.id, IPMSG_SENDMSG, reply);
//				// 用 C0 命令把图片内容发给飞秋
//				s = Buffer.concat([Buffer.from(s),
//					test_C0_msg.slice(i + 1)]);
//				udp_socket.send(s,
//					pal.port,
//					pal.IP, (err) => {
//				    if (err) {
//					fail ('发图片给飞秋出错: ' + err);
//					debug_info.scrollIntoView (false);
//				    }
//				});
//				// 此后检测到飞秋用 C1 命令来回应上述 C0 消息
//			}
//		};
//		try_send();
	    }
	    // 通知对方已收到消息
	    sendMsg(me, pal, IPMSG_RECVMSG, Msg.SN);
	    if (! logged) { // 好友首次被添加, 向其通告 Doe 离线, me 在线
		sendMsg(Doe, pal, IPMSG_BR_EXIT, Doe.nick);
		sendMsg(me, pal, IPMSG_BR_ENTRY, me.nick);
	    }
	    updateTable(Msg.id);
	    // 修正因 cipher 密文头部 \0 导致的 mainbody 错误
	    if (isMiao && Msg.mainbody == '' && sections[1] == 'cipher')
		Msg.mainbody = '\0cipher\0' + sections[2];
	    // 飞秋的消息，同时带有发送的文件名、长度、日期等信息
	    if (sections[1] && sections[1].replace(/\d+:.*$/, '') == '' &&
		sections[1].slice(-2) == ':\u0007') {
		//Msg.mainbody += '<br>发送了下面的文件或文件夹:<br>' +
		//	sections[1].replace(/\u0007/g, '<br>');
		Msg.mainbody += '\0' + sections[1];
	    }
	    // 删除飞秋消息尾部的字体、字号、颜色等信息
	    Msg.mainbody = Msg.mainbody
		.replace(/\{\/font;[\d -]{20,160}.{1,20} \d{1,20};\}/g, '');
	    // 延迟 10 秒, 等待对方有名字时再显示消息
	    {
		let tmp_time = Date.now();
		let intervalID = setInterval ( () => {
		    if (pal.nick != 'noName' ||
			pal.wgroup ||
			    Date.now() - tmp_time > 10000)
		    {
			clearInterval(intervalID);
			openChat (Msg);
		    }
		}, 500);
	    }
	    break;
	case IPMSG_RECVMSG: // 对方收到咱先前发出的消息后发回的收讫通知
	    updateTable(Msg.id); // 需要更新信息，因为至少 time 域已更改
	    // 标记消息已送达
	    let sn = Msg.mainbody.replace(/\0.*$/, '');
	    if (/^\d+$/.test(sn)) {
		let cb = RecvMsgCBs[sn];
		if (! cb || ! Array.isArray(cb) || cb.length != 2 ||
			typeof cb[0] !== 'function' ||
			typeof cb[1] !== 'number') {
		    if (cb)
			delete RecvMsgCBs[sn];
		} else {
		    cb[0] ();
		    RecvMsgCBs[sn][0] = null;
		    cb[0] = null;
		    delete RecvMsgCBs[sn];
		}
	    }
	    // 检查删除过期的项目
	    for (sn in RecvMsgCBs) {
		let cb = RecvMsgCBs[sn];
		if (! cb || ! Array.isArray(cb) || cb.length != 2 ||
			typeof cb[0] !== 'function' ||
			typeof cb[1] !== 'number') {
		    if (cb)
			delete RecvMsgCBs[sn]; // 删除无效项
		} else {
		    if (Date.now() - cb[1] > 300000)
			delete RecvMsgCBs[sn]; // 删除过期项
		}
	    }
	    break;
	case IPMSG_BR_EXIT: // 有人发来离线通告
	    if (Msg.mainbody != 'noName') {
		pal.nick = Msg.mainbody
			.replace(/[\u0000-,/:-@[-`{|}\u007f]/g, '_');
		if (sections[1])
		    pal.wgroup = sections[1]
			.replace(/[\u0000-,/:-@[-`{|}\u007f]/g, '_');
	    }
	    if (sections[2]) {
		let v = sections[2].split(/[\r\n]+/);
		for (let i = 0; i < v.length; i++) {
		    let tmp = v[i].slice(3)
				.replace(/[\u0000-,/:-@[-`{|}\u007f]/g, '_');
		    if (v[i].slice(0,3) == 'UN:') {
			if (tmp != 'Doe')
			    pal.login = tmp;
		    } else if (v[i].slice(0,3) == 'HN:') {
			if (tmp != 'noHost')
			    pal.host = tmp;
		    } else if (v[i].slice(0,3) == 'NN:')
			pal.nick = tmp;
		    else if (v[i].slice(0,3) == 'GN:')
			pal.wgroup = tmp;
		}
	    }

	    pal.offline = 1; // 标记对方离线

	    if (! isMiao) {
		updateTable(Msg.id);
		break;
	    }
	    if (sections[4]) {
		if (! isIPv4 && pal.myIPs != sections[4])
		    ip_changed = true;
		pal.myIPs = sections[4];
	    }
	    if (sections[5] && sections[5] != '255.255.255.255')
		pal.yourIP = sections[5];
	    if (sections[6] && sections[6] != '')
		pal.yourPort = sections[6];
	    if (sections[8]) {
		pal.services = sections[8];
	    }

	    //delete pal; // 删除好友会导致程序异常, 因此注释掉
	    updateTable(Msg.id);

	    if (! isIPv4) // 对方为 IPv6, 不为其转播离线消息
		break;
	    // 若对方在外网，替对方在本网转播一次离线消息

	    // 若对方 ip 不属于本地局域网，而且
	    // 对方看到的你的端口号不是 2425，
	    // 或者对方看到的你的 ip 不是局域网 ip
	    // 就说明对方是在外网
	    isOuter = (! isLocal && (pal.yourPort != 2425 ||
		ADDRs.split(',').indexOf(pal.yourIP) == -1));
	    if (stored_outer_LAN_IP &&
		stored_outer_LAN_IP.replace (/\/.*$/, '') != ip)
	    if (isOuter) {
		refresh(true, '\0' +
			ip + '\0' +
			rinfo.port + '\0' +
			pal.charset + '\0' +
			MSG);
	    } else if (ip == '127.0.0.1') {
	    } // to do: 支持安装在路由器上进行智能转发
	    break;
	//case IPMSG_READMSG: // 对方已打开消息; 飞秋接收文件时先发送此消息
	//    updateTable(Msg.id); // 需要更新信息，因为至少 time 域已更改
	//    break;
	default:
	    if (! logged || pal.nick == 'noName') { // 探测无名氏
		sendMsg(me, pal, IPMSG_BR_ENTRY, me.nick);
		let count = 20;
		let intervalID = setInterval ( () => {
		    if ( (count --) <= 0 || pal.offline ||
			(! pal.nick && pal.nick != 'noName'))
		    {
			clearInterval(intervalID);
		    	return;
		    }
		    sendMsg(me, pal, IPMSG_BR_ENTRY, me.nick);
		}, 30000);
	    }

	    // 接到不支持的飞秋命令, 调试输出
	    updateTable(Msg.id); // 虽然命令未知, 但表格要更新
	    if (cmd == 0) { // no operation
		// 消息主体是空的
		if (Msg.mainbody) { // 异常情况, 调试输出
		    fail (' 无操作【 0x' + Number(cmd).toString(16) +
			' 】, 来自 ' + pal.nick + '@' + ip +
			':' + rinfo.port + ' body=' + Msg.mainbody);
		}
		break;
	    }
	    if ((cmd & 255) == 0x04) // 【暂离】
	    {
		// 飞秋暂离的消息主体是: 昵称 + '[' + '暂离' 或 '工作' 等 + ']'
		if (Msg.mainbody.replace(/\[.+\]$/, '') != pal.nick) {
		    if (pal.nick == 'noName')
			pal.nick = Msg.mainbody.replace(/\[.+\]$/, '');
		    else if (pal.nick.replace(/_[^_]+_$/, '') !=
				Msg.mainbody.replace(/\[.+\]$/, ''))
		    // 异常情况, 调试输出
		    fail ('暂离【 0x' + Number(cmd).toString(16) +
			' 】, 来自 ' + pal.nick + '@' + ip +
			':' + rinfo.port + ' body=' + Msg.mainbody);
		}
		break;
	    }
	    if (cmd == 0x12) // get list
	    {
		// 这是飞鸽的查询好友列表命令
		fail ('查询好友列表【 0x' + Number(cmd).toString(16) +
			' 】, 来自 ' + pal.nick + '@' + ip +
			':' + rinfo.port + ' body=' + Msg.mainbody);
		break;
	    }
	    if ((cmd & 255) == 0x30) // 通知开封
	    {
		// 发文件给飞秋用户, 对方点击其中的一个文件, 就反馈 0x100030。
		// 对于同一批发送的多个文件, 此消息只反馈一次。
		// 消息主体是: 文件传送请求包的序号(10 进制)。
		if (! /^\d+$/.test(Msg.mainbody)) { // 异常情况, 调试输出
		    fail ('消息已读【 0x' + Number(cmd).toString(16) +
			' 】, 来自 ' + pal.nick + '@' + ip +
			':' + rinfo.port + ' body=' + Msg.mainbody);
		}
		break;
	    }
	    if (cmd == 0x72) // 查询公钥
	    {
		// 这是飞鸽的查询公钥命令, 飞秋似乎也用它来查询公钥
		// 消息主体是用16进制表示的加密方式（='21003'）
		// IPMSG_RSA_512	= 0x00001
		// IPMSG_RC2_40		= 0x01000
		// IPMSG_RSA_1024	= 0x00002
		// IPMSG_RC2_128	= 0x04000 // 飞秋无此项
		// IPMSG_BLOWFISH_128	= 0x20000
		if (Msg.mainbody != '21003') {	// 异常情况, 调试输出
		    fail ('查询公钥【 0x' + Number(cmd).toString(16) +
			' 】, 来自 ' + pal.nick + '@' + ip +
			':' + rinfo.port + ' body=' + Msg.mainbody);
		}
		// 某台飞秋电脑曾用如下的公钥来回应我的查询公钥请求,
		// 因此就收集到了这个合法的公钥。尾部的 key 有 256 个 hex 数字,
		// 因此公钥的实际长度是 128 字节, 也即 1024 位。
		// 现在就试试用这个公钥来应答,
		// 看看飞秋会有什么后续的反应。
		break; // 相当于注释掉下面的应答
		// 此应答导致 Windows 下无法接收飞秋发来的任何文件或文件夹!
		sendMsg(me, pal, 0x73,
'21003:10001-e0ba0e6d5c71d9886b99e52d030fc24358097cadf8583294a6e63a8f6d2ee63d023cfde28c50727f9c48a2bb81bf236dd83ba386f5e036a0e21bc96b1eb5d40b532d22c610b5136961dbe1947464b143603f79af932298600e515f43559a64e291013b071ea5932a668cebeeb72d2023868ce16e50ef0538aa2ba2bb83135de1'
		);
		break;
	    }
	    if (cmd == 0x73) // 响应请求, 发送公钥
	    {
		// 消息主体是公钥, 格式为
		// [cfg.smallPubKey.Capa]:[pubKey.Exponent]-[pubKey.Key]
		// 三个部分都是用16进制表示
		fail ('反馈公钥【 0x' + Number(cmd).toString(16) +
			' 】, 来自 ' + pal.nick + '@' + ip +
			':' + rinfo.port + ' body=' + Msg.mainbody);
		break;
	    }

	    let send_my_signature = function () {
		// 设置一个对其他飞秋用户友好的个性签名
		sendMsg(me, pal, 0x78,
			'我在用 miao, 不支持群聊, 不要拉我进群');
	    };

	    if (cmd == 0x77) // 查询对方个性签名
	    {
		// 消息主体是 '0'
		if (Msg.mainbody != '0') { // 异常情况, 调试输出
		    fail ('查询个性签名【 0x' + Number(cmd).toString(16) +
			' 】, 来自 ' + pal.nick + '@' + ip +
			':' + rinfo.port + ' body=' + Msg.mainbody);
		}
		send_my_signature ();
		break;
	    }
	    if (cmd == 0x78) // 消息主体是发送者的个性签名
	    {
		//fail ('反馈个性签名【 0x' + Number(cmd).toString(16) +
		//	' 】, 来自 ' + pal.nick + '@' + ip +
		//	':' + rinfo.port + ' body=' + Msg.mainbody);
		break;
	    }
	    if (cmd == 0x79) // 开始打字时发送此消息, 5 秒内不重复发送
	    {
		// 暂时忽略对方反馈的打字状态信息
		break;
	    }
	    if (cmd == 0x7a) // 已有 5 秒未打字时, 发送此消息
	    {
		// 暂时忽略对方反馈的打字状态信息
		break;
	    }
	    if (cmd == 0x94) // 未知命令
	    {
		// 消息主体是空的
		if (Msg.mainbody) { // 异常情况, 调试输出
		    fail ('命令【 0x' + Number(cmd).toString(16) +
			' 】, 来自 ' + pal.nick + '@' + ip +
			':' + rinfo.port + ' body=' + Msg.mainbody);
		}
		break;
	    }
	    if (cmd == 0xaa) // 未知命令
	    {
		// 消息主体是空的
		if (Msg.mainbody) { // 异常情况, 调试输出
		    fail ('命令【 0x' + Number(cmd).toString(16) +
			' 】, 来自 ' + pal.nick + '@' + ip +
			':' + rinfo.port + ' body=' + Msg.mainbody);
		}
		break;
	    }
	    if (cmd == 0xb0) // 请求远程协助
	    {
		// 消息主体是空的
		// 对方请求协助，我应该回应 0xba 表示响应此消息，
		// 然后再回应 0xb1 表示同意，或者 0xb2 表示拒绝。
		sendMsg(me, pal, 0xba, '');
		// 暂不支持远程协助，拒绝。若将来支持，则改成同意
		// sendMsg(me, pal, 0xb1, ''); // 同意
		sendMsg(me, pal, 0xb2, ''); // 拒绝

		if (Msg.mainbody) { // 异常情况, 调试输出
		    fail ('远程协助【 0x' + Number(cmd).toString(16) +
			' 】, 来自 ' + pal.nick + '@' + ip +
			':' + rinfo.port + ' body=' + Msg.mainbody +
			'<br/>原始消息:<br/>' + JSON.stringify(MSG)
			    .replace(/</g, '&lt;').replace(/>/g, '&gt;') +
			+ '<br/>' + msg.toString('hex') + '<br/>');
		}
		break;
	    }
	    if (cmd == 0xb1) // 同意对方发起的远程协助, 等待对方再次同意
	    {
		// 消息主体是空的
		// 如果再次确认时同意, 发送 0xb3, 表示同意建立协助连接
		// 如果再次确认时拒绝, 发送 0xb4
		// 暂不支持远程协助，拒绝。将来支持以后应改为同意
		// sendMsg(me, pal, 0xb3, ''); // 同意
		sendMsg(me, pal, 0xb4, ''); // 拒绝
		if (Msg.mainbody) { // 异常情况, 调试输出
		    fail ('同意协助【 0x' + Number(cmd).toString(16) +
			' 】, 来自 ' + pal.nick + '@' + ip +
			':' + rinfo.port + ' body=' + Msg.mainbody +
			'<br/>原始消息:<br/>' + JSON.stringify(MSG)
			    .replace(/</g, '&lt;').replace(/>/g, '&gt;') +
			+ '<br/>' + msg.toString('hex') + '<br/>');
		}
		break;
	    }
	    if (cmd == 0xb2) // 拒绝对方发起的远程协助
	    {
		// 消息主体是空的
		if (Msg.mainbody) { // 异常情况, 调试输出
		    fail ('拒绝协助【 0x' + Number(cmd).toString(16) +
			' 】, 来自 ' + pal.nick + '@' + ip +
			':' + rinfo.port + ' body=' + Msg.mainbody +
			'<br/>原始消息:<br/>' + JSON.stringify(MSG)
			    .replace(/</g, '&lt;').replace(/>/g, '&gt;') +
			+ '<br/>' + msg.toString('hex') + '<br/>');
		}
		break;
	    }
	    if (cmd == 0xb3) // 同意对方建立远程协助连接
	    {
		// 消息主体是空的
		if (Msg.mainbody) { // 异常情况, 调试输出
		    fail ('同意远程协助【 0x' + Number(cmd).toString(16) +
			' 】, 来自 ' + pal.nick + '@' + ip +
			':' + rinfo.port + ' body=' + Msg.mainbody +
			'<br/>原始消息:<br/>' + JSON.stringify(MSG)
			    .replace(/</g, '&lt;').replace(/>/g, '&gt;') +
			+ '<br/>' + msg.toString('hex') + '<br/>');
		}
		break;
	    }
	    if (cmd == 0xb4) // 取消自己发起的远程协助
	    {
		// 消息主体是空的
		if (Msg.mainbody) { // 异常情况, 调试输出
		    fail ('取消远程协助【 0x' + Number(cmd).toString(16) +
			' 】, 来自 ' + pal.nick + '@' + ip +
			':' + rinfo.port + ' body=' + Msg.mainbody +
			'<br/>原始消息:<br/>' + JSON.stringify(MSG)
			    .replace(/</g, '&lt;').replace(/>/g, '&gt;') +
			+ '<br/>' + msg.toString('hex') + '<br/>');
		}
		break;
	    }
	    if (cmd == 0xb5) // 申请控制，等待对方同意
	    {
		// 消息主体是空的
		// 对方请求控制，我应该回应 0xbc 表示响应此消息，
		// 然后再回应 0xb6 表示同意，或者 0xb7 表示拒绝。
		sendMsg(me, pal, 0xbc, '');
		// 暂不支持远程控制，拒绝。若将来支持，则改成同意
		// sendMsg(me, pal, 0xb6, ''); // 同意
		sendMsg(me, pal, 0xb7, ''); // 拒绝
		if (Msg.mainbody) { // 异常情况, 调试输出
		    fail ('申请控制【 0x' + Number(cmd).toString(16) +
			' 】, 来自 ' + pal.nick + '@' + ip +
			':' + rinfo.port + ' body=' + Msg.mainbody +
			'<br/>原始消息:<br/>' + JSON.stringify(MSG)
			    .replace(/</g, '&lt;').replace(/>/g, '&gt;') +
			+ '<br/>' + msg.toString('hex') + '<br/>');
		}
		break;
	    }
	    if (cmd == 0xb6) // 同意对方发起的远程控制, 等待对方再次同意
	    {
		// 消息主体是空的
		// 如果再次确认时同意, 发送 0xb8, 表示同意建立控制连接
		// 如果再次确认时拒绝, 发送 0xb9
		// 暂不支持远程控制，拒绝。将来支持以后应改为同意
		// sendMsg(me, pal, 0xb8, ''); // 同意
		sendMsg(me, pal, 0xb9, ''); // 拒绝
		if (Msg.mainbody) { // 异常情况, 调试输出
		    fail ('同意控制【 0x' + Number(cmd).toString(16) +
			' 】, 来自 ' + pal.nick + '@' + ip +
			':' + rinfo.port + ' body=' + Msg.mainbody +
			'<br/>原始消息:<br/>' + JSON.stringify(MSG)
			    .replace(/</g, '&lt;').replace(/>/g, '&gt;') +
			+ '<br/>' + msg.toString('hex') + '<br/>');
		}
		break;
	    }
	    if (cmd == 0xb7) // 拒绝对方的远程控制请求
	    {
		// 消息主体是空的
		if (Msg.mainbody) { // 异常情况, 调试输出
		    fail ('拒绝控制【 0x' + Number(cmd).toString(16) +
			' 】, 来自 ' + pal.nick + '@' + ip +
			':' + rinfo.port + ' body=' + Msg.mainbody +
			'<br/>原始消息:<br/>' + JSON.stringify(MSG)
			    .replace(/</g, '&lt;').replace(/>/g, '&gt;') +
			+ '<br/>' + msg.toString('hex') + '<br/>');
		}
		break;
	    }
	    if (cmd == 0xb8) // 同意对方建立远程控制连接
	    {
		// 消息主体是空的
		if (Msg.mainbody) { // 异常情况, 调试输出
		    fail ('同意远程控制【 0x' + Number(cmd).toString(16) +
			' 】, 来自 ' + pal.nick + '@' + ip +
			':' + rinfo.port + ' body=' + Msg.mainbody +
			'<br/>原始消息:<br/>' + JSON.stringify(MSG)
			    .replace(/</g, '&lt;').replace(/>/g, '&gt;') +
			+ '<br/>' + msg.toString('hex') + '<br/>');
		}
		break;
	    }
	    if (cmd == 0xb9) // 取消自己发起的远程控制
	    {
		// 消息主体是空的
		if (Msg.mainbody) { // 异常情况, 调试输出
		    fail ('准备控制【 0x' + Number(cmd).toString(16) +
			' 】, 来自 ' + pal.nick + '@' + ip +
			':' + rinfo.port + ' body=' + Msg.mainbody +
			'<br/>原始消息:<br/>' + JSON.stringify(MSG)
			    .replace(/</g, '&lt;').replace(/>/g, '&gt;') +
			+ '<br/>' + msg.toString('hex') + '<br/>');
		}
		break;
	    }
	    if (cmd == 0xba) // 已收到您的远程协助请求,正在处理
	    {
		// 消息主体是空的
		// 我发送 0xb0 命令后，对方回应 0xba, 可能是表示已收到。
		if (Msg.mainbody) { // 异常情况, 调试输出
		    fail ('收到协助请求【 0x' + Number(cmd).toString(16) +
			' 】, 来自 ' + pal.nick + '@' + ip +
			':' + rinfo.port + ' body=' + Msg.mainbody +
			'<br/>原始消息:<br/>' + JSON.stringify(MSG)
			    .replace(/</g, '&lt;').replace(/>/g, '&gt;') +
			+ '<br/>' + msg.toString('hex') + '<br/>');
		}
		break;
	    }
	    if (cmd == 0xbb) // 正在远程协助中,请稍后重试
	    {
		// 消息主体是空的
		if (Msg.mainbody) { // 异常情况, 调试输出
		    fail ('请求协助失败【 0x' + Number(cmd).toString(16) +
			' 】, 来自 ' + pal.nick + '@' + ip +
			':' + rinfo.port + ' body=' + Msg.mainbody +
			'<br/>原始消息:<br/>' + JSON.stringify(MSG)
			    .replace(/</g, '&lt;').replace(/>/g, '&gt;') +
			+ '<br/>' + msg.toString('hex') + '<br/>');
		}
		break;
	    }
	    if (cmd == 0xbc) // 已收到您的远程控制请求,正在处理
	    {
		// 消息主体是空的
		// 我发送 0xb5 命令后，对方回应 0xbc, 可能是表示已收到。
		if (Msg.mainbody) { // 异常情况, 调试输出
		    fail ('收到控制请求【 0x' + Number(cmd).toString(16) +
			' 】, 来自 ' + pal.nick + '@' + ip +
			':' + rinfo.port + ' body=' + Msg.mainbody +
			'<br/>原始消息:<br/>' + JSON.stringify(MSG)
			    .replace(/</g, '&lt;').replace(/>/g, '&gt;') +
			+ '<br/>' + msg.toString('hex') + '<br/>');
		}
		break;
	    }
	    if ((cmd & 255) == 0xc0) // 发送压缩且加密的图片数据
	    {
		// 图片是加密的, 如果不破解它, 无需调试输出
		//fail ('加密的图片【 0x' + Number(cmd).toString(16) +
		//	' 】, 来自 ' + pal.nick + '@' + ip +
		//	':' + rinfo.port + ', 原始消息:<br/>' +
		//	JSON.stringify(MSG).replace(/</g, '&lt;')
		//	.replace(/>/g, '&gt;') + '<br/>' + msg.toString('hex')
		//	+ '<br/>');
//		  // 消息主体 (Msg.mainbody) 的数据结构:
//		  // 图|图|本|片|本|本|0|图|0|00000000#\0本
//		  // 片 片 片 段 片 片   片              片
//		  // 代 总 起 总 序 长   类              数
//		  // 号 长 始 数 号 度   型              据
//		  // b2b811e2|210621|0|412|1|512|0|2|0|00000000#\0图片片段
//		  // b2b811e2|210621|512|412|2|512|0|2|0|00000000#\0图片片段
//		  // b2b811e2|210621|1024|412|3|512|0|2|0|00000000#\0图片片段
//		  // 图片类型 1 代表 bmp
//		  // 图片类型 2 代表 JPG
//		test_C0_msg = msg;
//		// 用 C1 命令回复飞秋, 告知图片片段已收到
//		let v = Msg.mainbody.split('|');
//		if (v.length < 10)
//			break;
//		sendMsg(me, Msg.id, 0xC1, v[0] + '|' + v[4] + '#');
		break;
	    }
	    if (cmd == 0xc6)	// 有人创建群, 会发送此消息
	    {
		// 与群有关的消息几乎全是加密的, 如果不破解它, 无需调试输出
		//fail ('命令【 0x' + Number(cmd).toString(16) +
		//	' 】, 来自 ' + pal.nick + '@' + ip +
		//	':' + rinfo.port + ', 原始消息:<br/>' +
		//	JSON.stringify(MSG).replace(/</g, '&lt;')
		//	.replace(/>/g, '&gt;') + '<br/>' + msg.toString('hex')
		//	+ '<br/>');
		break;
	    }
	    if (cmd == 0x2000c9)	// 有人创建群, 会发送此消息
	    {
		// 与群有关的消息几乎全是加密的, 如果不破解它, 无需调试输出
		//fail ('群聊【 0x' + Number(cmd).toString(16) +
		//	' 】, 来自 ' + pal.nick + '@' + ip +
		//	':' + rinfo.port);
		break;
	    }
	    if (cmd == 0xca)	// 有人创建群, 会发送此消息
	    {
		// 与群有关的消息几乎全是加密的, 如果不破解它, 无需调试输出
		//fail ('群聊【 0x' + Number(cmd).toString(16) +
		//	' 】, 来自 ' + pal.nick + '@' + ip +
		//	':' + rinfo.port + ' len=' + msg.length);
		break;
	    }
	    if (cmd == 0xcd) // 刷新群成员
	    {
		// 消息主体是群号, 未加密, 但无需调试输出
		//fail ('群号【 0x' + Number(cmd).toString(16) +
		//	' 】, 来自 ' + pal.nick + '@' + ip +
		//	':' + rinfo.port + ' body=' + Msg.mainbody);
		break;
	    }

	    let send_my_pal_list = function () {
		// 转换 ip 为整数
		let ip_to_int = function (ip) {
		    let v = ip.split ('.');
		    return (parseInt(v[0]) |
			(parseInt(v[1]) << 8) |
			(parseInt(v[2]) << 16) |
			(parseInt(v[3]) << 24)) >>> 0 ;
		};

		let list = '';
		for (let id in Pal) {
		    if (Pal[id].IP) {
			list += (ip_to_int (Pal[id].IP) + '\u0007');
		    }
		}
		sendMsg(me, pal, 0xd5, list); // 发送好友列表
		return list;
	    };

	    if ((cmd & 255) == 0xd4) // 对方查询我的好友列表
	    {
		// 消息主体是空的, 或数字 '0' 
		if (Msg.mainbody && Msg.mainbody != '0') { //异常情况, 调试输出
		    fail ('查询好友列表【 0x' + Number(cmd).toString(16) +
			' 】, 来自 ' + pal.nick + '@' + ip +
			':' + rinfo.port + ' body=' + Msg.mainbody);
		}
		let list = send_my_pal_list ();
		break;
	    }

	    if ((cmd & 255) == 0xd5) // 对方反馈的好友列表
	    {
		//let v = Msg.mainbody.split('\u0007');
		//let list = '';
		//for (let i in v) {
		//    if (! v[i]) break;
		//    let ip = parseInt(v[i]);
		//    list += (ip & 255) + '.' + ((ip >> 8) & 255) + '.' +
		//    ((ip >> 16) & 255) + '.' + ((ip >> 24) & 255) + '<br/>';
		//}
		//fail ('反馈好友列表【 0x' + Number(cmd).toString(16) +
		//	' 】, 来自 ' + pal.nick + '@' + ip +
		//	':' + rinfo.port + ' 好友列表:<br/>' + list);
		break;
	    }
	    if ((cmd & 255) == 0xd6) // 查询相应于 MAC 地址的用户资料
	    {
		// 消息主体是	(MAC 地址) + '_USERZL_USERZL'
		// 或		(MAC 地址) + '_USERZL'

		let MAC = stored_randomID.slice(0,12);
		if (Msg.mainbody == MAC + '_USERZL_USERZL' ||
		    Msg.mainbody == MAC + '_USERZL') {
		    // 响应对方的查询, 可以填写这些资料, 反馈给飞秋
		    sendMsg(me, pal, 0xd7, Msg.mainbody +
			    '$#&*办公电话#~$#住宅电话#~$#手机#~$#邮箱#~$#' +
			    '地址#~$#其他(如QQ)#~$#');
		} else { // 异常情况, 调试输出
		    fail ('查询用户资料【 0x' + Number(cmd).toString(16) +
			' 】, 来自 ' + pal.nick + '@' + ip +
			':' + rinfo.port + ' body=' + Msg.mainbody);
		}
		//send_my_signature ();
		//send_my_pal_list ();
		break;
	    }
	    if ((cmd & 255) == 0xd7) // 反馈用户资料
	    {
		// 消息主体是
		//	(接收者 MAC 地址) + '_USERZL' (+ 可选的'_USERZL') +
		//		'$#&*办公电话#~$#住宅电话#~$#手机#~$#' +
		//		'邮箱#~$#地址#~$#其他(如QQ)#~$#'

		let MAC = stored_randomID.slice(0,12);
		if (Msg.mainbody != MAC + '_USERZL_USERZL' &&
		    Msg.mainbody != MAC + '_USERZL') { // 异常情况, 调试输出
		    fail ('反馈用户资料【 0x' + Number(cmd).toString(16) +
			' 】, 来自 ' + pal.nick + '@' + ip +
			':' + rinfo.port + ' body=' + Msg.mainbody);
		}
		break;
	    }
	    if ((cmd & 511) == 0x1d8) // 飞秋打招呼, 探测对方信息
	    {
		// 消息主体是数字 '0'
		// 当我发出上线消息探测飞秋的时候, 飞秋在给出应答消息 0x600003
		// 之前, 还发送 1d8 命令
		// 当飞秋上线诊断我是否在线的时候, 它发送命令 0, 0x600001, 1d8
		// 当我发送 1d8 命令时, 飞秋回应 0x78 （个性签名）

		if (Msg.mainbody != '0') { // 异常情况, 调试输出
		    fail ('探测对方信息【 0x' + Number(cmd).toString(16) +
			' 】, 来自 ' + pal.nick + '@' + ip +
			':' + rinfo.port + ' body=' + Msg.mainbody);
		}
		//sendMsg(me, pal, 0xd4, '0'); // 查询对方好友列表
		//send_my_signature ();
		//send_my_pal_list ();
		//sendMsg(me, pal, 0x77, '0');
		//sendMsg(me, pal, 0x1d8, '0');
		//sendMsg(me, pal, 0, '');
		//sendMsg(me, pal, 0x94, ''); // 调试feiq未知命令
		//sendMsg(me, pal, 0xb0, ''); // 调试feiq远程协助
		break;
	    }
	    if ((cmd & 255) == 0xd9)
	    {
		// 消息主体是 (接收者 MAC 地址), 
		// 在回应 1d8 的同时, 这台主机又回应 d9,
		// 可能是请求与 MAC 地址对应的用户(也就是我)的某项参数
		if (Msg.mainbody != stored_randomID.slice(0,12)) {
		    // 异常情况, 调试输出
		    fail ('命令【 0x' + Number(cmd).toString(16) +
			' 】, 来自 ' + pal.nick + '@' + ip +
			':' + rinfo.port);
		}
		break;
	    }
	    // 调试输出, 看看会出现哪些未知的新命令
	    fail ('不支持的命令【 0x' + Number(cmd).toString(16) +
		' 】, 来自 ' + pal.nick + '@' + ip +
		':' + rinfo.port + ', 原始消息:<br/>' +
		JSON.stringify(MSG).replace(/</g, '&lt;').replace(/>/g, '&gt;')
		+ '<br/>' + msg.toString('hex') + '<br/>');
	    break;
	} // switch (cmd & 255)
	if (ip_changed)
	    update_friends ();
    }); // udp_socket.on('message', ...)

    //updateMyNetInfo ();
    //updateTable();
    // 每隔 10 秒执行一次 udp_socket_open 函数，尝试绑定 2425 端口。
    var udp_socket_IntervalID = setInterval (udp_socket_open, 10000);
    var tcp_server_IntervalID = setInterval (tcp_server_open, 10000);

    // setInterval 不立即执行一次，必须额外写一句才行。
    udp_socket_open ();
    tcp_server_open ();

    var last_mail_time = 0;
    var mailFetchInterval = 0;
    var imapConnectIntervalID;
    if (typeof startupImapInterval !== 'undefined' ||
	(stored_mailuser && stored_mailpass))
	//imapConnectIntervalID = setInterval (imapConnect, 60000);
	if (miaoconfig.startupMailFetchWait >= 0)
	setMailFetchInterval(1000 * miaoconfig.startupMailFetchWait);

    // 软件启动后执行一次同步检查
    //dir_sync_start(); // 改为在刷新好友时执行

    // 启动 http 服务

    // 先创建 wwwroot 文件夹，使其与 miao 文件夹和 shared 文件夹并列

    const wwwroot = path.resolve('..', 'wwwroot');
    if (! mkdir_recursive(wwwroot)) {
	fail (' WWW: 创建文件夹 ' + wwwroot + ' 失败, 无法启动服务! ');
	break label_window;
    }

    //const chalk = require('./node_modules/chalk');
    const httpServer = require('./node_modules/http-server/lib/http-server');
    const portfinder = require('./node_modules/portfinder');
    //const opener     = require('./node_modules/opener');
    //const url        = require('url');
    //const argv = require('./node_modules/minimist')(process.argv.slice(2),
	//		{
	//			alias: {
	//				tls: 'ssl'
	//			}
	//		}
	//	);
    //const ifaces = os.networkInterfaces();

    //let port = argv.p || argv.port || parseInt(process.env.PORT, 10);
    //let host = argv.a || '0.0.0.0';
    //let tls = argv.S || argv.tls;
    //let sslPassphrase = process.env.NODE_HTTP_SERVER_SSL_PASSPHRASE;
    //let proxy = argv.P || argv.proxy;
    //let proxyOptions = argv['proxy-options'];
    //let utc = argv.U || argv.utc;
    //let version = argv.v || argv.version;
    //let logger;

    //var proxyOptionsBooleanProps = [
    //  'ws', 'xfwd', 'secure', 'toProxy', 'prependPath', 'ignorePath',
    //  'changeOrigin', 'preserveHeaderKeyCase', 'followRedirects',
    //  'selfHandleResponse'
    //];

    //if (proxyOptions) {
    //  Object.keys(proxyOptions).forEach(function (key) {
    //    if (proxyOptionsBooleanProps.indexOf(key) > -1) {
    //      proxyOptions[key] = proxyOptions[key].toLowerCase() === 'true';
    //    }
    //  });
    //}

    //if (chalk) {
    //  logger = {
    //    info: function () {},
    //    request: function () {}
    //  };
    //}

    //if (version) {
    //  logger.info('v' + require('../package.json').version);
    //  process.exit();
    //}

    //if (!port) {
    //  portfinder.basePort = 8080;
    //  portfinder.getPort(function (err, port) {
    //    if (err) { throw err; }
    //    listen(port);
    //  });
    //} else {
    //  listen(port);
    //}

    const listen = function (port) {
	const options = {
	    root:		wwwroot, //argv._[0],
	    cache:		-1, //argv.c,
	    timeout:		180, // 禁用超时，默认是 120, //argv.t,
	    showDir:		true, //argv.d,
	    autoIndex:		true, //argv.i,
	    gzip:		false, //argv.g || argv.gzip,
	    brotli:		false, //argv.b || argv.brotli,
	    robots:		false, //argv.r || argv.robots,
	    //ext: argv.e || argv.ext,
	    logFn:		() => {}, // logger.request,
	    //proxy: proxy,
	    //proxyOptions: proxyOptions,
	    showDotfiles:	true, //argv.dotfiles,
	    //mimetypes: argv.mimetypes,
	    //username: argv.username || process.env.NODE_HTTP_SERVER_USERNAME,
	    //password: argv.password || process.env.NODE_HTTP_SERVER_PASSWORD
	};

	//if (argv.cors) {
	//    options.cors = true;
	//    if (typeof argv.cors === 'string') {
	//	options.corsHeaders = argv.cors;
	//    }
	//}

	//if (proxy) {
	//    try {
	//	new url.URL(proxy);
	//    } catch (err) {
	//	logger.info(chalk.red('Error: Invalid proxy url'));
	//	process.exit(1);
	//    }
	//}

	//if (tls) {
	//    options.https = {
	//	cert: argv.C || argv.cert || 'cert.pem',
	//	key: argv.K || argv.key || 'key.pem',
	//	passphrase: sslPassphrase,
	//    };
	//    try {
	//	fs.lstatSync(options.https.cert);
	//    } catch (err) {
	//	logger.info(chalk.red('Error: Could not find certificate ' +
	//		options.https.cert));
	//	process.exit(1);
	//    }
	//    try {
	//	fs.lstatSync(options.https.key);
	//    } catch (err) {
	//	logger.info(chalk.red('Error: Could not find private key ' +
	//		options.https.key));
	//	process.exit(1);
	//    }
	//}

	const server = httpServer.createServer(options);

	server.listen(port, '::', function () {
	    //fail(' 成功启动 http 服务, 端口: ' + port);
	//    const protocol = tls ? 'https://' : 'http://';

	//    logger.info([
	//	chalk.yellow('Starting up http-server, serving '),
	//	chalk.cyan(server.root),
	//	tls ? (chalk.yellow(' through') + chalk.cyan(' https')) : ''
	//	].join(''));

	//    logger.info([
	//	chalk.yellow('\nhttp-server version: '),
	//	chalk.cyan(require('../package.json').version)
	//	].join(''));

	//    logger.info([
	//	chalk.yellow('\nhttp-server settings: '),
	//	([chalk.yellow('CORS: '),
	//		argv.cors ? chalk.cyan(argv.cors) :
	//			chalk.red('disabled')].join('')),
	//	([chalk.yellow('Cache: '),
	//		argv.c ? (argv.c === '-1' ? chalk.red('disabled') :
	//			chalk.cyan(argv.c + ' seconds')) :
	//				chalk.cyan('3600 seconds')].join('')),
	//	([chalk.yellow('Connection Timeout: '),
	//		argv.t === '0' ? chalk.red('disabled') :
	//			(argv.t ? chalk.cyan(argv.t + ' seconds') :
	//				chalk.cyan('120 seconds'))].join('')),
	//	([chalk.yellow('Directory Listings: '),
	//		argv.d ? chalk.red('not visible') :
	//			chalk.cyan('visible')].join('')),
	//	([chalk.yellow('AutoIndex: '),
	//		argv.i ? chalk.red('not visible') :
	//			chalk.cyan('visible')].join('')),
	//	([chalk.yellow('Serve GZIP Files: '),
	//		(argv.g || argv.gzip) ? chalk.cyan('true') :
	//			chalk.red('false')].join('')),
	//	([chalk.yellow('Serve Brotli Files: '),
	//		(argv.b || argv.brotli) ? chalk.cyan('true') :
	//			chalk.red('false')].join('')),
	//	([chalk.yellow('Default File Extension: '),
	//		argv.e ? chalk.cyan(argv.e) :
	//			(argv.ext ? chalk.cyan(argv.ext) :
	//				chalk.red('none'))].join(''))
	//	].join('\n'));

	//    logger.info(chalk.yellow('\nAvailable on:'));

	//    if (argv.a && host !== '0.0.0.0') {
	//	logger.info(
	//		`  ${protocol}${host}:${chalk.green(port.toString())}`
	//	);
	//    } else {
	//	Object.keys(ifaces).forEach(function (dev) {
	//	    ifaces[dev].forEach(function (details) {
	//		if (details.family === 'IPv4') {
	//		    logger.info(('  ' + protocol + details.address +
	//			':' + chalk.green(port.toString())));
	//		}
	//	    });
	//	});
	//    }

	//    if (typeof proxy === 'string') {
	//	if (proxyOptions) {
	//	    logger.info('Unhandled requests will be served from: ' +
	//		proxy + '. Options: ' + JSON.stringify(proxyOptions));
	//	} else {
	//	    logger.info('Unhandled requests will be served from: ' +
	//		proxy);
	//	}
	//    }

	//    logger.info('Hit CTRL-C to stop the server');

	//    if (argv.o) {
	//	const openHost = host === '0.0.0.0' ? '127.0.0.1' : host;
	//	let openUrl = `${protocol}${openHost}:${port}`;
	//	if (typeof argv.o === 'string') {
	//	    openUrl += argv.o[0] === '/' ? argv.o : '/' + argv.o;
	//	}
	//	logger.info('Open: ' + openUrl);
	//	opener(openUrl);
	//    }

	//    // Spacing before logs
	//    if (!argv.s)
	//	logger.info();
	});
    };

    portfinder.basePort = 5080;
    portfinder.getPort(function (err, port) {
	if (err) {
	    //throw err;
	    fail(' WWW: 获取空闲端口失败, 服务无法启动! ' + err);
	    return;
	}
	if (port != 5080) {
	    fail(' WWW: 端口 5080 被占用，http 服务启动失败! ');
	    return;
	}
	listen(port);
    });

if (false) // 调试时设置为 true
setTimeout(()=>{
	let options = {};
	//options.protocol = 'http:';
	options.host = 'localhost';
	options.port = 5080;
	options.path = encodeURI('/中文.txt');
	options.method = 'GET'; // 'HEAD';
	fail(options.path);
    let req = http.request(options, function(res) {
	fail('response:<br/>' + JSON.stringify(res.headers));
	fail('response:<br/>' + (typeof res.headers['content-length']));
	let content = '';
	if (res.statusCode === 200) {
	    res.on('error', function(err) {
		fail(err);
	    });
	    res.on('data', function(chunk) {
		content += chunk;
	    });
	    res.on('end', function() {
		fail(content);
	    });
	} else {
	    fail('Server respond ' + res.statusCode);
	}
    });

    req.end();
    req.on('error', function(err) {
	fail(err);
    });
}, 15000);

    /////////////////////////////////////////////////////////////////////////
    // Windows 对 WebDAV 的支持很糟糕，因此，WebDAV 实际上用处不大, 屏蔽掉。
    // 注释掉下面这行，即可开启 WebDAV 服务。

    break label_window;
    // 上面这行，若在其开头添加双斜杠注释符，即启用后续的 WebDAV 代码。
    // 若启用 WebDAV, 应同时启用打开网盘的按钮。搜'网盘'可以找到那段代码。
    /////////////////////////////////////////////////////////////////////////

    // 启动 WebDAV 服务（http 协议）

    // 先创建 webdav 文件夹，使其与 miao 文件夹和 shared 文件夹并列

    tmp = path.resolve('..', 'webdav');
    if (! mkdir_recursive(tmp)) {
	fail (' WebDAV: 创建文件夹 ' + tmp + ' 失败, 无法启动服务! ');
	break label_window;
    }

    const webdav = require('./node_modules/webdav-server');

    webdav.PhysicalFolder.loadFromPath(tmp, (e, folder) => {
	if(e) {
	    fail(' WebDAV: 加载 webdav 文件夹失败! ' + e);
	    // throw e;
	    return;
	}

	//const selfsigned = require('./node_modules/selfsigned');
	//const pems = selfsigned.generate();

	const server = new webdav.WebDAVServer({
		//https: {
		//	key:	pems.private,
		//	cert:	pems.cert,
		//},
		port: 5050 // http
	});

	server.addResourceTree(folder, (e) => {
	    if(e) {
		fail(' WebDAV: 添加资源树 ' + folder + ' 失败! ' + e);
		// throw e;
		return;
	    }

	    server.start((s) => {
		fail(' 成功启动 WebDAV 服务, 端口: ' + s.address().port);
	    });
	});
    });

    /////////////////////////////////////////////////////////////////////////
    // 好不容易搞定了 https, 但 Windows 还是不认, 这做的都是无用功, 屏蔽掉
    break label_window; // 相当于屏蔽掉后续的 WebDAV - https 代码
    /////////////////////////////////////////////////////////////////////////

    // 启动 WebDAV 服务（https 协议）

    // 以下是 https 代码，启用前应先屏蔽前面的 http 代码，并把所有的 5050
    // 端口号都改为 5051，包括按钮的提示信息中的端口号。

    // 参考了 Alexander Wells 的《用纯 NodeJS 代码生成证书》 
    //		Certificate Generate Pure NodeJS
    // https://node-security.com/posts/certificate-generation-pure-nodejs/

    const forge = require('./node_modules/node-forge');

    const makeNumberPositive = (hexString) => {
	let tmp = parseInt(hexString[0], 16);

	if (tmp < 8)
	    return hexString;

	tmp -= 8;
	return tmp.toString() + hexString.substring(1);
    };

    // Generate a random serial number for the Certificate
    const randomSerialNumber = () => {
	return makeNumberPositive(forge.util.bytesToHex(forge.random.getBytesSync(20)));
    };

    // Get the Not Before Date for a Certificate (will be valid from 2 days ago)
    const getCertNotBefore = () => {
	let twoDaysAgo = new Date(Date.now() - 60*60*24*2*1000);
	let year = twoDaysAgo.getFullYear();
	let month = (twoDaysAgo.getMonth() + 1).toString().padStart(2, '0');
	let day = twoDaysAgo.getDate();
	return new Date(`${year}-${month}-${day} 00:00:00Z`);
    };

    // Get Certificate Expiration Date (Valid for 90 Days)
    const getCertNotAfter = (notBefore) => {
	let ninetyDaysLater = new Date(notBefore.getTime() + 60*60*24*90*1000);
	let year = ninetyDaysLater.getFullYear();
	let month = (ninetyDaysLater.getMonth() + 1).toString().padStart(2, '0');
	let day = ninetyDaysLater.getDate();
	return new Date(`${year}-${month}-${day} 23:59:59Z`);
    };

    // Get CA Expiration Date (Valid for 100 Years)
    const getCANotAfter = (notBefore) => {
	let year = notBefore.getFullYear() + 100;
	let month = (notBefore.getMonth() + 1).toString().padStart(2, '0');
	let day = notBefore.getDate();
	return new Date(`${year}-${month}-${day} 23:59:59Z`);
    };

    const DEFAULT_C = 'Australia';
    const DEFAULT_ST = 'Victoria';
    const DEFAULT_L = 'Melbourne';

    class CertificateGeneration {
	static CreateRootCA() {
		// Create a new Keypair for the Root CA
		const { privateKey, publicKey } =
			forge.pki.rsa.generateKeyPair(2048);

		// Define the attributes for the new Root CA
		const attributes = [{
			shortName: 'C',
			value: DEFAULT_C
		}, {
			shortName: 'ST',
			value: DEFAULT_ST
		}, {
			shortName: 'L',
			value: DEFAULT_L
		}, {
			shortName: 'CN',
			value: 'My Custom Testing RootCA'
		}];

		const extensions = [{
			name: 'basicConstraints',
			cA: true
		}, {
			name: 'keyUsage',
			keyCertSign: true,
			cRLSign: true
		}];

		// Create an empty Certificate
		const cert = forge.pki.createCertificate();

		// Set the Certificate attributes for the new Root CA
		cert.publicKey = publicKey;
		cert.privateKey = privateKey;
		cert.serialNumber = randomSerialNumber();
		cert.validity.notBefore = getCertNotBefore();
		cert.validity.notAfter = getCANotAfter(cert.validity.notBefore);
		cert.setSubject(attributes);
		cert.setIssuer(attributes);
		cert.setExtensions(extensions);

		// Self-sign the Certificate
		cert.sign(privateKey, forge.md.sha512.create());

		// Convert to PEM format
		const pemCert = forge.pki.certificateToPem(cert);
		const pemKey = forge.pki.privateKeyToPem(privateKey);

		// Return the PEM encoded cert and private key
		return {
			certificate:	pemCert,
			privateKey:	pemKey,
			notBefore:	cert.validity.notBefore,
			notAfter:	cert.validity.notAfter
		};
	}

	static CreateHostCert(hostCertCN, validDomains, rootCAObject) {
		if (! hostCertCN.toString().trim())
			throw new Error('"hostCertCN" must be a String');
		if (! Array.isArray(validDomains))
			throw new Error('"validDomains" must be an Array of Strings');
		if (! rootCAObject ||
		    ! rootCAObject.hasOwnProperty('certificate') ||
		    ! rootCAObject.hasOwnProperty('privateKey'))
			throw new Error('"rootCAObject" must be an Object with the properties "certificate" & "privateKey"');

		// Convert the Root CA PEM details, to a forge Object
		let caCert = forge.pki.certificateFromPem(rootCAObject.certificate);
		let caKey = forge.pki.privateKeyFromPem(rootCAObject.privateKey);

		// Create a new Keypair for the Host Certificate
		const hostKeys = forge.pki.rsa.generateKeyPair(2048);

		// Define the attributes/properties for the Host Certificate
		const attributes = [{
			shortName: 'C',
			value: DEFAULT_C
		}, {
			shortName: 'ST',
			value: DEFAULT_ST
		}, {
			shortName: 'L',
			value: DEFAULT_L
		}, {
			shortName: 'CN',
			value: hostCertCN
		}, {
			name: 'organizationName',
			value: 'MyCompany'
		}, {
			shortName: 'OU',
			value: 'MyUnit'
		}];

		const extensions = [{
			name: 'basicConstraints',
			cA: false
		}, {
			name: 'nsCertType',
			server: true
		}, {
			name: 'subjectKeyIdentifier'
		}, {
			name: 'authorityKeyIdentifier',
			authorityCertIssuer: true,
			serialNumber: caCert.serialNumber
		}, {
			name: 'keyUsage',
			digitalSignature: true,
			nonRepudiation: true,
			keyEncipherment: true
		}, {
			name: 'extKeyUsage',
			serverAuth: true
		}, {
			name: 'subjectAltName',
			altNames: validDomains.map(domain => {
				return {
					type:	2,
					value:	domain
				};
			})
		}];

		// Create an empty Certificate
		let newHostCert = forge.pki.createCertificate();

		// Set the attributes for the new Host Certificate
		newHostCert.publicKey = hostKeys.publicKey;
		newHostCert.serialNumber = randomSerialNumber();
		newHostCert.validity.notBefore = getCertNotBefore();
		newHostCert.validity.notAfter =
			getCertNotAfter(newHostCert.validity.notBefore);

		newHostCert.setSubject(attributes);
		newHostCert.setIssuer(caCert.subject.attributes);
		newHostCert.setExtensions(extensions);

		// Sign the new Host Certificate using the CA
		newHostCert.sign(caKey, forge.md.sha512.create());

		// Convert to PEM format
		let pemHostCert = forge.pki.certificateToPem(newHostCert);
		let pemHostKey = forge.pki.privateKeyToPem(hostKeys.privateKey);

		return {
			certificate:	pemHostCert,
			privateKey:	pemHostKey,
			notBefore:	newHostCert.validity.notBefore,
			notAfter:	newHostCert.validity.notAfter
		};
	}
    }

    let CA = CertificateGeneration.CreateRootCA();

    // The following certificate:
    //	- Will be called 'testing.com'.
    //	- Will be valid for 'testing.com' and 'test.com'.
    //	- Will be signed by the CA we just created above.

    let hostIPs = [];

    let host = (CIDRs + ',' + GUAs).split(',');
    //fail(host);
    for (let i = 0; i < host.length; i++) {
	if (! host[i])
		continue;
	hostIPs.push(host[i].replace(/\/\d+$/, ''));
    }
    //fail(hostIPs);

    let hostCert = CertificateGeneration.CreateHostCert(
			'*', //'testing.com',
			hostIPs, //[
			//	'testing.com',
			//	'test.com'
			//],
			CA
		);

    //fail(CA.certificate);
    //fail(hostCert.certificate);
    
    webdav.PhysicalFolder.loadFromPath(tmp, (e, folder) => {
	if(e) {
	    fail(' WebDAV: 加载 webdav 文件夹失败! ' + e);
	    // throw e;
	    return;
	}

	//const selfsigned = require('./node_modules/selfsigned');
	//const pems = selfsigned.generate();
	////fail(JSON.stringify(pems));

	const server = new webdav.WebDAVServer({
		https: {
			ca:	CA.certificate,
			cert:	hostCert.certificate,
			key:	hostCert.privateKey,
		},
		port: 5051
	});

	server.addResourceTree(folder, (e) => {
	    if(e) {
		fail(' WebDAV: 添加资源树 ' + folder + ' 失败! ' + e);
		// throw e;
		return;
	    }

	    server.start((s) => {
		fail(' 成功启动 WebDAV 服务, 端口: ' + s.address().port);
	    });
	});
    });

// IRC 支持测试
//var irc = require('./node-irc');
//
//var bot = new irc.Client('irc.dollyfish.net.nz', 'nodebot', {
//    //debug: true,
//    channels: ['#test', '#othertest']
//});
//
//bot.addListener('error', function(message) {
//    fail ('ERROR: ' + message.command + ': ' + message.args.join(' '));
//});
//
//// 监听 raw 事件会显示大量信息, 一般不需要监听
////bot.addListener('raw', function(message) {
////    fail ('raw: ' + JSON.stringify(message)
////				.replace(/</, '&lt;')
////				.replace(/>/, '&gt;'));
////});
//
//bot.addListener('message#blah', function(from, message) {
//    fail (' ' + from + ': ' + message);
//});
//
//bot.addListener('message', function(from, to, message) {
//    fail (' ' + from + '@' + to + ': ' + message);
//
//    if (to.match(/^[#&]/)) {	// channel message
//        if (message.match(/hello/i)) {
//            bot.say(to, 'Hello there ' + from);
//        }
//    }
//    else {	// private message
//        fail ('private message');
//    }
//});
//bot.addListener('pm', function(nick, message) {
//    fail ('Got private message from ' + nick + ': ' + message);
//});
//bot.addListener('join', function(channel, who) {
//    fail (' ' + who + ' has joined ' + channel);
//});
//bot.addListener('part', function(channel, who, reason) {
//    fail (' ' + who + ' has left ' + channel + ': ' + reason);
//});
//bot.addListener('kick', function(channel, who, by, reason) {
//    fail (' ' + who + ' was kicked from' + channel + ' by ' + by +
//		': ' + reason);
//});
//// 测试完成后及时断开连接, 不占用服务器资源
//setTimeout ( () => { bot.disconnect('Closing session'); }, 30000);

    // 主窗口代码结束
}
else // if (opener != null && (typeof nw !== 'object' || opener != global))
{
    // 这是聊天窗口，即主窗口的子窗口。聊天窗口在初始时是一个空白页面，然后
    // 立即用 js 代码动态地写入内容。由于初始页面是空的，因此，这里没有放置
    // 代码。写入动态内容所需的 js 代码已经嵌入到主窗口的代码里面了。
}

</script>

<!-- 这条注释用于终结前面 js 代码开启的注释，不能删除！ -->

</body>
</html>

